From f80b1063e404d28779a7c78cfd616fd416a2ef10 Mon Sep 17 00:00:00 2001
From: russkel
Date: Sat, 8 Jun 2013 14:53:27 +0900
Subject: Update submitting-patches.txt
Few quick fixes to the terminology in order to match what is used on the Django Trac at the moment.
---
docs/internals/contributing/writing-code/submitting-patches.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'docs')
diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt
index ed8aad99b3..8f8ddddab4 100644
--- a/docs/internals/contributing/writing-code/submitting-patches.txt
+++ b/docs/internals/contributing/writing-code/submitting-patches.txt
@@ -37,7 +37,7 @@ and time availability), claim it by following these steps:
`ticket tracker`_.
* If a ticket for this issue already exists, make sure nobody else has
- claimed it. To do this, look at the "Assigned to" section of the ticket.
+ claimed it. To do this, look at the "Owned by" section of the ticket.
If it's assigned to "nobody," then it's available to be claimed.
Otherwise, somebody else is working on this ticket, and you either find
another bug/feature to work on, or contact the developer working on the
@@ -48,7 +48,7 @@ and time availability), claim it by following these steps:
* Claim the ticket:
- 1. click the "accept" radio button under "Action" near the bottom of the
+ 1. click the "assign to myself" radio button under "Action" near the bottom of the
page,
2. then click "Submit changes."
--
cgit v1.3
From 6a092f24e01b6448f4e5601968db82720c9397de Mon Sep 17 00:00:00 2001
From: Claude Paroz
Date: Sat, 8 Jun 2013 15:05:50 +0200
Subject: Allow build of translated documentation
---
docs/Makefile | 3 ++-
docs/conf.py | 3 +++
2 files changed, 5 insertions(+), 1 deletion(-)
(limited to 'docs')
diff --git a/docs/Makefile b/docs/Makefile
index 2a8bcd7101..a2c926c7ea 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -6,11 +6,12 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
+LANGUAGE =
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees -D language=$(LANGUAGE) $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
diff --git a/docs/conf.py b/docs/conf.py
index a01ddb60b8..feff99b6f4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -77,6 +77,9 @@ django_next_version = '1.6'
# for a list of supported languages.
#language = None
+# Location for .po/.mo translation files used when language is set
+locale_dirs = ['locale/']
+
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
--
cgit v1.3
From 706eced9bb9012788494c14566577ed691924e82 Mon Sep 17 00:00:00 2001
From: Camilo Nova
Date: Sat, 8 Jun 2013 10:45:39 -0500
Subject: Update flatpages.txt
Fixed typo
---
docs/ref/contrib/flatpages.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'docs')
diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt
index 292b304acb..cff3f680d4 100644
--- a/docs/ref/contrib/flatpages.txt
+++ b/docs/ref/contrib/flatpages.txt
@@ -47,7 +47,7 @@ Then either:
3. Add an entry in your URLconf. For example::
urlpatterns = patterns('',
- ('^pages/', include('django.contrib.flatpages.urls')),
+ (r'^pages/', include('django.contrib.flatpages.urls')),
)
or:
@@ -74,7 +74,7 @@ There are several ways to include the flat pages in your URLconf. You can
dedicate a particular path to flat pages::
urlpatterns = patterns('',
- ('^pages/', include('django.contrib.flatpages.urls')),
+ (r'^pages/', include('django.contrib.flatpages.urls')),
)
You can also set it up as a "catchall" pattern. In this case, it is important
--
cgit v1.3
From 4bd6e5e89501680235ded6c460657a1231231931 Mon Sep 17 00:00:00 2001
From: Kenny Rachuonyo
Date: Sun, 9 Jun 2013 08:23:48 +0300
Subject: Update media.txt
Fixed typo: froms -> forms
---
docs/topics/forms/media.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'docs')
diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt
index b014e97119..a1fd533241 100644
--- a/docs/topics/forms/media.txt
+++ b/docs/topics/forms/media.txt
@@ -49,7 +49,7 @@ define the media requirements.
Here's a simple example::
- from django import froms
+ from django import forms
class CalendarWidget(forms.TextInput):
class Media:
--
cgit v1.3
From 175a102ddceae2ebd1a4e03916a23adafc35bda5 Mon Sep 17 00:00:00 2001
From: Tim Graham
Date: Thu, 6 Jun 2013 14:15:26 -0400
Subject: Fixed #20567 - Documented BoundField.id_for_label.
Thanks littlepig for the suggestion.
---
docs/ref/forms/api.txt | 23 +++++++++++++++++++++++
docs/ref/forms/widgets.txt | 5 ++++-
2 files changed, 27 insertions(+), 1 deletion(-)
(limited to 'docs')
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 67e3aab712..c05430c64e 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -688,6 +688,29 @@ by a ``Widget``::
>>> print(bound_form['subject'].value())
hi
+.. attribute:: BoundField.id_for_label
+
+Use this property to render the ID of this field. For example, if you are
+manually constructing a ```` in your template (despite the fact that
+:meth:`~BoundField.label_tag` will do this for you):
+
+.. code-block:: html+django
+
+ ... {{ my_field }}
+
+By default, this will be the field's name prefixed by ``id_``
+("``id_my_field``" for the example above). You may modify the ID by setting
+:attr:`~django.forms.Widget.attrs` on the field's widget. For example,
+declaring a field like this::
+
+ my_field = forms.CharField(widget=forms.TextInput(attrs={'id': 'myFIELD'}))
+
+and using the template above, would render something like:
+
+.. code-block:: html
+
+ ...
+
.. _binding-uploaded-files:
Binding uploaded files to a form
diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
index 0f6917d44c..341574ad37 100644
--- a/docs/ref/forms/widgets.txt
+++ b/docs/ref/forms/widgets.txt
@@ -163,6 +163,9 @@ Django will then include the extra attributes in the rendered output:
Url:
Comment:
+You can also set the HTML ``id`` using :attr:`~Widget.attrs`. See
+:attr:`BoundField.id_for_label` for an example.
+
.. _styling-widget-classes:
Styling widget classes
@@ -251,7 +254,7 @@ foundation for custom widgets.
into two separate values::
from django.forms import MultiWidget
-
+
class SplitDateTimeWidget(MultiWidget):
# ...
--
cgit v1.3
From 4e94c84e50b960405d5708d8d9528c44c7dabe83 Mon Sep 17 00:00:00 2001
From: Tim Graham
Date: Sun, 9 Jun 2013 15:05:15 -0400
Subject: Fixed #19875 - Added warnings regarding DEBUG=False and empty
ALLOWED_HOSTS
---
docs/intro/tutorial03.txt | 7 +++++++
docs/ref/settings.txt | 4 ++++
docs/topics/settings.txt | 5 +++++
3 files changed, 16 insertions(+)
(limited to 'docs')
diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt
index 120369172e..6193ec45f7 100644
--- a/docs/intro/tutorial03.txt
+++ b/docs/intro/tutorial03.txt
@@ -473,6 +473,13 @@ template for all 404 errors when :setting:`DEBUG` is set to ``False`` (in your
settings module). If you do create the template, add at least some dummy
content like "Page not found".
+.. warning::
+
+ If :setting:`DEBUG` is set to ``False``, all responses will be
+ "Bad Request (400)" unless you specify the proper :setting:`ALLOWED_HOSTS`
+ as well (something like ``['localhost', '127.0.0.1']`` for
+ local development).
+
A couple more things to note about 404 views:
* If :setting:`DEBUG` is set to ``True`` (in your settings module) then your
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index ef52d3170c..897af275a0 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -852,6 +852,10 @@ It is also important to remember that when running with :setting:`DEBUG`
turned on, Django will remember every SQL query it executes. This is useful
when you're debugging, but it'll rapidly consume memory on a production server.
+Finally, if :setting:`DEBUG` is ``False``, you also need to properly set
+the :setting:`ALLOWED_HOSTS` setting. Failing to do so will result in all
+requests being returned as "Bad Request (400)".
+
.. _django/views/debug.py: https://github.com/django/django/blob/master/django/views/debug.py
.. setting:: DEBUG_PROPAGATE_EXCEPTIONS
diff --git a/docs/topics/settings.txt b/docs/topics/settings.txt
index fa26297988..1cfcd262cd 100644
--- a/docs/topics/settings.txt
+++ b/docs/topics/settings.txt
@@ -17,6 +17,11 @@ Here are a couple of example settings::
DEFAULT_FROM_EMAIL = 'webmaster@example.com'
TEMPLATE_DIRS = ('/home/templates/mike', '/home/templates/john')
+.. note::
+
+ If you set :setting:`DEBUG` to ``False``, you also need to properly set
+ the :setting:`ALLOWED_HOSTS` setting.
+
Because a settings file is a Python module, the following apply:
* It doesn't allow for Python syntax errors.
--
cgit v1.3
From 4daf570b98cc840e1a154f3876bc7463924cb9ae Mon Sep 17 00:00:00 2001
From: Aymeric Augustin
Date: Tue, 4 Jun 2013 08:13:36 +0200
Subject: Added TransactionTestCase.available_apps.
This can be used to make Django's test suite significantly faster by
reducing the number of models for which content types and permissions
must be created and tables must be flushed in each non-transactional
test.
It's documented for Django contributors and committers but it's branded
as a private API to preserve our freedom to change it in the future.
Most of the credit goes to Anssi. He got the idea and did the research.
Fixed #20483.
---
django/contrib/auth/management/__init__.py | 15 ++++++--
django/contrib/contenttypes/management.py | 7 +++-
django/db/models/__init__.py | 2 +-
django/db/models/loading.py | 62 ++++++++++++++++++++++++------
django/test/testcases.py | 44 ++++++++++++++-------
docs/topics/testing/overview.txt | 28 ++++++++++++++
6 files changed, 127 insertions(+), 31 deletions(-)
(limited to 'docs')
diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
index fdf822ff74..ecf6c1b635 100644
--- a/django/contrib/auth/management/__init__.py
+++ b/django/contrib/auth/management/__init__.py
@@ -11,7 +11,7 @@ from django.contrib.auth import models as auth_app, get_user_model
from django.core import exceptions
from django.core.management.base import CommandError
from django.db import DEFAULT_DB_ALIAS, router
-from django.db.models import get_models, signals
+from django.db.models import get_model, get_models, signals, UnavailableApp
from django.utils.encoding import DEFAULT_LOCALE_ENCODING
from django.utils import six
from django.utils.six.moves import input
@@ -60,6 +60,11 @@ def _check_permission_clashing(custom, builtin, ctype):
pool.add(codename)
def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
+ try:
+ get_model('auth', 'Permission')
+ except UnavailableApp:
+ return
+
if not router.allow_syncdb(db, auth_app.Permission):
return
@@ -101,9 +106,13 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
def create_superuser(app, created_models, verbosity, db, **kwargs):
- from django.core.management import call_command
+ try:
+ get_model('auth', 'Permission')
+ UserModel = get_user_model()
+ except UnavailableApp:
+ return
- UserModel = get_user_model()
+ from django.core.management import call_command
if UserModel in created_models and kwargs.get('interactive', True):
msg = ("\nYou just installed Django's auth system, which means you "
diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py
index ddd7654ed7..64d1c418ef 100644
--- a/django/contrib/contenttypes/management.py
+++ b/django/contrib/contenttypes/management.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, router
-from django.db.models import get_apps, get_models, signals
+from django.db.models import get_apps, get_model, get_models, signals, UnavailableApp
from django.utils.encoding import smart_text
from django.utils import six
from django.utils.six.moves import input
@@ -11,6 +11,11 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
Creates content types for models in the given app, removing any model
entries that no longer have a matching model class.
"""
+ try:
+ get_model('contenttypes', 'ContentType')
+ except UnavailableApp:
+ return
+
if not router.allow_syncdb(db, ContentType):
return
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 3eac2167d4..b5dd1a58bc 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
+from django.db.models.loading import get_apps, 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 5ad2f12ba0..40aa983c25 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -15,6 +15,8 @@ import os
__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
'load_app', 'app_cache_ready')
+class UnavailableApp(Exception):
+ pass
class AppCache(object):
"""
@@ -43,6 +45,7 @@ class AppCache(object):
postponed=[],
nesting_level=0,
_get_models_cache={},
+ available_apps=None,
)
def __init__(self):
@@ -135,12 +138,17 @@ class AppCache(object):
"""
self._populate()
+ apps = self.app_store.items()
+ if self.available_apps is not None:
+ apps = [elt for elt in apps
+ if self._label_for(elt[0]) in self.available_apps]
+
# Ensure the returned list is always in the same order (with new apps
# added at the end). This avoids unstable ordering on the admin app
# list page, for example.
- apps = [(v, k) for k, v in self.app_store.items()]
- apps.sort()
- return [elt[1] for elt in apps]
+ apps = sorted(apps, key=lambda elt: elt[1])
+
+ return [elt[0] for elt in apps]
def get_app_paths(self):
"""
@@ -161,8 +169,12 @@ class AppCache(object):
def get_app(self, app_label, emptyOK=False):
"""
- Returns the module containing the models for the given app_label. If
- the app has no models in it and 'emptyOK' is True, returns None.
+ Returns the module containing the models for the given app_label.
+
+ Returns None if the app has no models in it and emptyOK is True.
+
+ Raises UnavailableApp when set_available_apps() in in effect and
+ doesn't include app_label.
"""
self._populate()
imp.acquire_lock()
@@ -170,12 +182,11 @@ class AppCache(object):
for app_name in settings.INSTALLED_APPS:
if app_label == app_name.split('.')[-1]:
mod = self.load_app(app_name, False)
- if mod is None:
- if emptyOK:
- return None
+ if mod is None and not emptyOK:
raise ImproperlyConfigured("App with label %s is missing a models.py module." % app_label)
- else:
- return mod
+ if self.available_apps is not None and app_label not in self.available_apps:
+ raise UnavailableApp("App with label %s isn't available." % app_label)
+ return mod
raise ImproperlyConfigured("App with label %s could not be found" % app_label)
finally:
imp.release_lock()
@@ -209,8 +220,13 @@ class AppCache(object):
include_swapped, they will be.
"""
cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
+ model_list = None
try:
- return self._get_models_cache[cache_key]
+ model_list = self._get_models_cache[cache_key]
+ if self.available_apps is not None and only_installed:
+ model_list = [m for m in model_list
+ if m._meta.app_label in self.available_apps]
+ return model_list
except KeyError:
pass
self._populate()
@@ -235,6 +251,9 @@ class AppCache(object):
(not model._meta.swapped or include_swapped))
)
self._get_models_cache[cache_key] = model_list
+ if self.available_apps is not None and only_installed:
+ model_list = [m for m in model_list
+ if m._meta.app_label in self.available_apps]
return model_list
def get_model(self, app_label, model_name,
@@ -244,12 +263,21 @@ class AppCache(object):
model_name.
Returns None if no model is found.
+
+ Raises UnavailableApp when set_available_apps() in in effect and
+ doesn't include app_label.
"""
if seed_cache:
self._populate()
if only_installed and app_label not in self.app_labels:
return None
- return self.app_models.get(app_label, SortedDict()).get(model_name.lower())
+ if (self.available_apps is not None and only_installed
+ and app_label not in self.available_apps):
+ raise UnavailableApp("App with label %s isn't available." % app_label)
+ try:
+ return self.app_models[app_label][model_name.lower()]
+ except KeyError:
+ return None
def register_models(self, app_label, *models):
"""
@@ -274,6 +302,16 @@ class AppCache(object):
model_dict[model_name] = model
self._get_models_cache.clear()
+ def set_available_apps(self, available):
+ if not set(available).issubset(set(settings.INSTALLED_APPS)):
+ extra = set(available) - set(settings.INSTALLED_APPS)
+ raise ValueError("Available apps isn't a subset of installed "
+ "apps, extra apps: " + ", ".join(extra))
+ self.available_apps = set(app.rsplit('.', 1)[-1] for app in available)
+
+ def unset_available_apps(self):
+ self.available_apps = None
+
cache = AppCache()
# These methods were always module level, so are kept that way for backwards
diff --git a/django/test/testcases.py b/django/test/testcases.py
index 08c03154b1..1dbb82e352 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -28,6 +28,7 @@ from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
WSGIServerException)
from django.core.urlresolvers import clear_url_caches, set_urlconf
from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction
+from django.db.models.loading import cache
from django.forms.fields import CharField
from django.http import QueryDict
from django.test.client import Client
@@ -725,6 +726,9 @@ class TransactionTestCase(SimpleTestCase):
# test case
reset_sequences = False
+ # Subclasses can enable only a subset of apps for faster tests
+ available_apps = None
+
def _pre_setup(self):
"""Performs any pre-test setup. This includes:
@@ -733,7 +737,14 @@ class TransactionTestCase(SimpleTestCase):
named fixtures.
"""
super(TransactionTestCase, self)._pre_setup()
- self._fixture_setup()
+ if self.available_apps is not None:
+ cache.set_available_apps(self.available_apps)
+ try:
+ self._fixture_setup()
+ except Exception:
+ if self.available_apps is not None:
+ cache.unset_available_apps()
+ raise
def _databases_names(self, include_mirrors=True):
# If the test case has a multi_db=True flag, act on all databases,
@@ -775,22 +786,27 @@ class TransactionTestCase(SimpleTestCase):
* Force closing the connection, so that the next test gets
a clean cursor.
"""
- self._fixture_teardown()
- super(TransactionTestCase, self)._post_teardown()
- # Some DB cursors include SQL statements as part of cursor
- # creation. If you have a test that does rollback, the effect
- # of these statements is lost, which can effect the operation
- # of tests (e.g., losing a timezone setting causing objects to
- # be created with the wrong time).
- # To make sure this doesn't happen, get a clean connection at the
- # start of every test.
- for conn in connections.all():
- conn.close()
+ try:
+ self._fixture_teardown()
+ super(TransactionTestCase, self)._post_teardown()
+ # Some DB cursors include SQL statements as part of cursor
+ # creation. If you have a test that does rollback, the effect of
+ # these statements is lost, which can effect the operation of
+ # tests (e.g., losing a timezone setting causing objects to be
+ # created with the wrong time). To make sure this doesn't happen,
+ # get a clean connection at the start of every test.
+ for conn in connections.all():
+ conn.close()
+ finally:
+ cache.unset_available_apps()
def _fixture_teardown(self):
+ # Allow TRUNCATE ... CASCADE when flushing only a subset of the apps
+ allow_cascade = self.available_apps is not None
for db_name in self._databases_names(include_mirrors=False):
- call_command('flush', verbosity=0, interactive=False, database=db_name,
- skip_validation=True, reset_sequences=False)
+ call_command('flush', verbosity=0, interactive=False,
+ database=db_name, skip_validation=True,
+ reset_sequences=False, allow_cascade=allow_cascade)
def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True):
items = six.moves.map(transform, qs)
diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt
index 2b1db5e501..6f73f7bbb5 100644
--- a/docs/topics/testing/overview.txt
+++ b/docs/topics/testing/overview.txt
@@ -985,6 +985,34 @@ to test the effects of commit and rollback:
Using ``reset_sequences = True`` will slow down the test, since the primary
key reset is an relatively expensive database operation.
+.. attribute:: TransactionTestCase.available_apps
+
+ .. warning::
+
+ This attribute is a private API. It may be changed or removed without
+ a deprecation period in the future, for instance to accomodate changes
+ in application loading.
+
+ It's used to optimize Django's own test suite, which contains hundreds
+ of models but no relations between models in different applications.
+
+ .. versionadded:: 1.6
+
+ By default, ``available_apps`` is set to ``None`` and has no effect.
+ Setting it to a list of applications tells Django to behave as if only the
+ models from these applications were available:
+
+ - Before each test, Django creates content types and permissions only for
+ these models.
+ - After each test, Django flushes only the corresponding tables. However,
+ at the database level, truncation may cascade to other related models,
+ even if they aren't in ``available_apps``.
+
+ Since the database isn't fully flushed, if a test creates instances of
+ models not included in ``available_apps``, they will leak and they may
+ cause unrelated tests to fail. Be careful with tests that use sessions;
+ the default session engine stores them in the database.
+
TestCase
~~~~~~~~
--
cgit v1.3
From 584bd14dcfdee9585fec7794d53ce120ea73d0bc Mon Sep 17 00:00:00 2001
From: Gabe Jackson
Date: Fri, 8 Jun 2012 15:32:35 +0200
Subject: Fixed #18134 -- BoundField.label_tag now includes the form's
label_suffix
There was an inconsistency between how the label_tag for forms were
generated depending on which method was used: as_p, as_ul and as_table
contained code to append the label_suffix where as label_tag called on a
form field directly did NOT append the label_suffix. The code for
appending the label_suffix has been moved in to the label_tag code of
the field and the HTML generation code for as_p, as_ul and as_table now
calls this code as well.
This is a backwards incompatible change because users who have added the
label_suffix manually in their templates may now get double label_suffix
characters in their forms.
---
django/forms/forms.py | 9 ++++----
docs/ref/forms/api.txt | 9 +++++++-
docs/releases/1.6.txt | 31 +++++++++++++++++++++++++
docs/topics/forms/index.txt | 16 +++++++++----
docs/topics/forms/modelforms.txt | 2 +-
tests/admin_util/tests.py | 4 ++--
tests/forms_tests/tests/test_forms.py | 36 ++++++++++++++---------------
tests/forms_tests/tests/test_regressions.py | 4 ++--
8 files changed, 77 insertions(+), 34 deletions(-)
(limited to 'docs')
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 0c598ac775..e6a11f28fb 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -170,11 +170,6 @@ class BaseForm(object):
if bf.label:
label = conditional_escape(force_text(bf.label))
- # Only add the suffix if the label does not end in
- # punctuation.
- if self.label_suffix:
- if label[-1] not in ':?.!':
- label = format_html('{0}{1}', label, self.label_suffix)
label = bf.label_tag(label) or ''
else:
label = ''
@@ -522,6 +517,10 @@ class BoundField(object):
If attrs are given, they're used as HTML attributes on the tag.
"""
contents = contents or self.label
+ # Only add the suffix if the label does not end in punctuation.
+ if self.form.label_suffix:
+ if contents[-1] not in ':?.!':
+ contents = format_html('{0}{1}', contents, self.form.label_suffix)
widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id
if id_:
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index c05430c64e..3c17827800 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -498,6 +498,8 @@ include ``%s`` -- then the library will act as if ``auto_id`` is ``True``.
By default, ``auto_id`` is set to the string ``'id_%s'``.
+.. attribute:: Form.label_suffix
+
Normally, a colon (``:``) will be appended after any label name when a form is
rendered. It's possible to change the colon to another character, or omit it
entirely, using the ``label_suffix`` parameter::
@@ -650,12 +652,17 @@ To separately render the label tag of a form field, you can call its
>>> f = ContactForm(data)
>>> print(f['message'].label_tag())
- Message
+ Message:
Optionally, you can provide the ``contents`` parameter which will replace the
auto-generated label tag. An optional ``attrs`` dictionary may contain
additional attributes for the ```` tag.
+.. versionchanged:: 1.6
+
+ The label now includes the form's :attr:`~django.forms.Form.label_suffix`
+ (a semicolon, by default).
+
.. method:: BoundField.css_classes()
When you use Django's rendering shortcuts, CSS classes are used to
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index 6736af8c2d..e5cd3273dc 100644
--- a/docs/releases/1.6.txt
+++ b/docs/releases/1.6.txt
@@ -581,6 +581,37 @@ It is still possible to convert the fetched rows to ``Model`` objects
lazily by using the :meth:`~django.db.models.query.QuerySet.iterator()`
method.
+:meth:`BoundField.label_tag` now includes the form's :attr:`~django.forms.Form.label_suffix`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is consistent with how methods like
+:meth:`Form.as_p` and
+:meth:`Form.as_ul` render labels.
+
+If you manually render ``label_tag`` in your templates:
+
+.. code-block:: html+django
+
+ {{ form.my_field.label_tag }}: {{ form.my_field }}
+
+you'll want to remove the semicolon (or whatever other separator you may be
+using) to avoid duplicating it when upgrading to Django 1.6. The following
+template in Django 1.6 will render identically to the above template in Django
+1.5, except that the semicolon will appear inside the ```` element.
+
+.. code-block:: html+django
+
+ {{ form.my_field.label_tag }} {{ form.my_field }}
+
+will render something like:
+
+.. code-block:: html
+
+ My Field:
+
+If you want to keep the current behavior of rendering ``label_tag`` without
+the ``label_suffix``, instantiate the form ``label_suffix=''``.
+
Miscellaneous
~~~~~~~~~~~~~
diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
index ac58acc9e3..7d5a0ed411 100644
--- a/docs/topics/forms/index.txt
+++ b/docs/topics/forms/index.txt
@@ -302,7 +302,7 @@ loop::
{% for field in form %}
{{ field.errors }}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
{% endfor %}
@@ -316,8 +316,14 @@ attributes, which can be useful in your templates:
The label of the field, e.g. ``Email address``.
``{{ field.label_tag }}``
- The field's label wrapped in the appropriate HTML ```` tag,
- e.g. ``Email address ``
+ The field's label wrapped in the appropriate HTML ```` tag.
+
+ .. versionchanged:: 1.6
+
+ This includes the form's :attr:`~django.forms.Form.label_suffix`. For
+ example, the default ``label_suffix`` is a semicolon::
+
+ Email address:
``{{ field.value }}``
The value of the field. e.g ``someone@example.com``
@@ -375,7 +381,7 @@ these two methods::
{% for field in form.visible_fields %}
{{ field.errors }}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
{% endfor %}
@@ -403,7 +409,7 @@ using the :ttag:`include` tag to reuse it in other templates::
{% for field in form %}
{{ field.errors }}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
{% endfor %}
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 4c46c6c0c0..d325a15cca 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -907,7 +907,7 @@ Third, you can manually render each field::
{{ formset.management_form }}
{% for form in formset %}
{% for field in form %}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
{% endfor %}
{% endfor %}
diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py
index 4a9a203f50..637f643261 100644
--- a/tests/admin_util/tests.py
+++ b/tests/admin_util/tests.py
@@ -301,7 +301,7 @@ class UtilTests(SimpleTestCase):
self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
'text : ')
self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
- 'cb ')
+ 'cb : ')
# normal strings needs to be escaped
class MyForm(forms.Form):
@@ -312,7 +312,7 @@ class UtilTests(SimpleTestCase):
self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
'&text: ')
self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
- '&cb ')
+ '&cb: ')
def test_flatten_fieldsets(self):
"""
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 1a3fb44a66..3b722e5ac1 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -1589,9 +1589,9 @@ class FormsTestCase(TestCase):
# Recall from above that passing the "auto_id" argument to a Form gives each
# field an "id" attribute.
t = Template('''''')
self.assertHTMLEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """""")
self.assertHTMLEqual(t.render(Context({'form': UserRegistration(auto_id='id_%s')})), """""")
# User form.[field].help_text to output a field's help text. If the given field
# does not have help text, nothing will be output.
t = Template('''''')
self.assertHTMLEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """
"""
% {'blue_pk': colour.pk})
+ def test_custom_error_messages(self) :
+ data = {'name1': '@#$!!**@#$', 'name2': '@#$!!**@#$'}
+ errors = CustomErrorMessageForm(data).errors
+ self.assertHTMLEqual(
+ str(errors['name1']),
+ 'Form custom error message. '
+ )
+ self.assertHTMLEqual(
+ str(errors['name2']),
+ 'Model custom error message. '
+ )
+
class M2mHelpTextTest(TestCase):
"""Tests for ticket #9321."""
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index 49389ef663..a1555d8e91 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -214,8 +214,8 @@ class TestSimpleValidators(TestCase):
def test_message_dict(self):
v = ValidationError({'first': ['First Problem']})
- self.assertEqual(str(v), str_prefix("{%(_)s'first': %(_)s'First Problem'}"))
- self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': %(_)s'First Problem'})"))
+ self.assertEqual(str(v), str_prefix("{%(_)s'first': [%(_)s'First Problem']}"))
+ self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': [%(_)s'First Problem']})"))
test_counter = 0
for validator, value, expected in TEST_DATA:
--
cgit v1.3
From 2c4fe761a0e2b28e2c5c3b4bc506ee06824a443d Mon Sep 17 00:00:00 2001
From: Erik Romijn
Date: Mon, 17 Jun 2013 18:06:26 +0200
Subject: Fixed #20593 -- Allow blank passwords in check_password() and
set_password()
---
django/contrib/auth/hashers.py | 12 +++----
django/contrib/auth/tests/test_hashers.py | 53 +++++++++++++++++++++++++++++++
docs/ref/contrib/auth.txt | 16 ++++++++++
docs/releases/1.6.txt | 9 ++++++
docs/topics/auth/customizing.txt | 16 ++++++++++
docs/topics/auth/passwords.txt | 6 ++++
6 files changed, 106 insertions(+), 6 deletions(-)
(limited to 'docs')
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 36d247d71e..d8a04e1473 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -47,7 +47,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
If setter is specified, it'll be called when you need to
regenerate the password.
"""
- if not password or not is_password_usable(encoded):
+ if not is_password_usable(encoded):
return False
preferred = get_hasher(preferred)
@@ -65,10 +65,10 @@ def make_password(password, salt=None, hasher='default'):
Turn a plain-text password into a hash for database storage
Same as encode() but generates a new random salt. If
- password is None or blank then UNUSABLE_PASSWORD will be
+ password is None then UNUSABLE_PASSWORD will be
returned which disallows logins.
"""
- if not password:
+ if password is None:
return UNUSABLE_PASSWORD
hasher = get_hasher(hasher)
@@ -222,7 +222,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
digest = hashlib.sha256
def encode(self, password, salt, iterations=None):
- assert password
+ assert password is not None
assert salt and '$' not in salt
if not iterations:
iterations = self.iterations
@@ -350,7 +350,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
algorithm = "sha1"
def encode(self, password, salt):
- assert password
+ assert password is not None
assert salt and '$' not in salt
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
@@ -378,7 +378,7 @@ class MD5PasswordHasher(BasePasswordHasher):
algorithm = "md5"
def encode(self, password, salt):
- assert password
+ assert password is not None
assert salt and '$' not in salt
hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py
index 4becc2dffe..9df6bd8592 100644
--- a/django/contrib/auth/tests/test_hashers.py
+++ b/django/contrib/auth/tests/test_hashers.py
@@ -32,6 +32,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
+ # Blank passwords
+ blank_encoded = make_password('')
+ self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_pkbdf2(self):
encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@@ -41,6 +47,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256')
+ self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_sha1(self):
encoded = make_password('lètmein', 'seasalt', 'sha1')
@@ -50,6 +62,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'sha1')
+ self.assertTrue(blank_encoded.startswith('sha1$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_md5(self):
encoded = make_password('lètmein', 'seasalt', 'md5')
@@ -59,6 +77,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "md5")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'md5')
+ self.assertTrue(blank_encoded.startswith('md5$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_md5(self):
encoded = make_password('lètmein', '', 'unsalted_md5')
@@ -72,6 +96,11 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(alt_encoded))
self.assertTrue(check_password('lètmein', alt_encoded))
self.assertFalse(check_password('lètmeinz', alt_encoded))
+ # Blank passwords
+ blank_encoded = make_password('', '', 'unsalted_md5')
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_sha1(self):
encoded = make_password('lètmein', '', 'unsalted_sha1')
@@ -83,6 +112,12 @@ class TestUtilsHashPass(unittest.TestCase):
# Raw SHA1 isn't acceptable
alt_encoded = encoded[6:]
self.assertFalse(check_password('lètmein', alt_encoded))
+ # Blank passwords
+ blank_encoded = make_password('', '', 'unsalted_sha1')
+ self.assertTrue(blank_encoded.startswith('sha1$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
@@ -92,6 +127,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmei', encoded))
self.assertFalse(check_password('lètmeiz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
+ # Blank passwords
+ blank_encoded = make_password('', 'ab', 'crypt')
+ self.assertTrue(blank_encoded.startswith('crypt$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt_sha256(self):
@@ -108,6 +149,12 @@ class TestUtilsHashPass(unittest.TestCase):
encoded = make_password(password, hasher='bcrypt_sha256')
self.assertTrue(check_password(password, encoded))
self.assertFalse(check_password(password[:72], encoded))
+ # Blank passwords
+ blank_encoded = make_password('', hasher='bcrypt_sha256')
+ self.assertTrue(blank_encoded.startswith('bcrypt_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt(self):
@@ -117,6 +164,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
+ # Blank passwords
+ blank_encoded = make_password('', hasher='bcrypt')
+ self.assertTrue(blank_encoded.startswith('bcrypt$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unusable(self):
encoded = make_password(None)
diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt
index 40b3629f63..afbc6ec048 100644
--- a/docs/ref/contrib/auth.txt
+++ b/docs/ref/contrib/auth.txt
@@ -132,12 +132,28 @@ Methods
password hashing. Doesn't save the
:class:`~django.contrib.auth.models.User` object.
+ When the ``raw_password`` is ``None``, the password will be set to an
+ unusable password, as if
+ :meth:`~django.contrib.auth.models.User.set_unusable_password()`
+ were used.
+
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally stored
+ as an unsable password.
+
.. method:: check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
comparison.)
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally
+ considered to be an unusable password, resulting in this method
+ returning ``False`` for such a password.
+
.. method:: set_unusable_password()
Marks the user as having no password set. This isn't the same as
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index f260a1eac0..8dea37d2da 100644
--- a/docs/releases/1.6.txt
+++ b/docs/releases/1.6.txt
@@ -701,6 +701,15 @@ Miscellaneous
* :class:`~django.views.generic.base.RedirectView` now has a `pattern_name`
attribute which allows it to choose the target by reversing the URL.
+* In Django 1.4 and 1.5, a blank string was unintentionally not considered to
+ be a valid password. This meant
+ :meth:`~django.contrib.auth.models.User.set_password()` would save a blank
+ password as an unusable password like
+ :meth:`~django.contrib.auth.models.User.set_unusable_password()` does, and
+ thus :meth:`~django.contrib.auth.models.User.check_password()` always
+ returned ``False`` for blank passwords. This has been corrected in this
+ release: blank passwords are now valid.
+
Features deprecated in 1.6
==========================
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index bc021b14ad..ea4ec8686d 100644
--- a/docs/topics/auth/customizing.txt
+++ b/docs/topics/auth/customizing.txt
@@ -583,12 +583,28 @@ The following methods are available on any subclass of
password hashing. Doesn't save the
:class:`~django.contrib.auth.models.AbstractBaseUser` object.
+ When the raw_password is ``None``, the password will be set to an
+ unusable password, as if
+ :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()`
+ were used.
+
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally stored
+ as an unsable password as well.
+
.. method:: models.AbstractBaseUser.check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
comparison.)
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally
+ considered to be an unusable password, resulting in this method
+ returning ``False`` for such a password.
+
.. method:: models.AbstractBaseUser.set_unusable_password()
Marks the user as having no password set. This isn't the same as
diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
index 206e7d856c..7e4b59a99c 100644
--- a/docs/topics/auth/passwords.txt
+++ b/docs/topics/auth/passwords.txt
@@ -206,6 +206,12 @@ from the ``User`` model.
database to check against, and returns ``True`` if they match, ``False``
otherwise.
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally considered
+ to be an unusable password, resulting in this method returning
+ ``False`` for such a password.
+
.. function:: make_password(password[, salt, hashers])
Creates a hashed password in the format used by this application. It takes
--
cgit v1.3
From c86a9b63984f6692d478f6f70e3c78de4ec41814 Mon Sep 17 00:00:00 2001
From: Loic Bistuer
Date: Wed, 19 Jun 2013 02:41:36 +0700
Subject: Fixed #6903 - Preserve admin changelist filters after saving or
deleting an object
---
django/contrib/admin/options.py | 107 ++++++++++----
.../contrib/admin/templates/admin/change_form.html | 8 +-
.../contrib/admin/templates/admin/change_list.html | 6 +-
.../admin/templates/admin/delete_confirmation.html | 3 +-
.../admin/delete_selected_confirmation.html | 3 +-
.../admin/templates/admin/object_history.html | 3 +-
.../contrib/admin/templates/admin/submit_line.html | 5 +-
django/contrib/admin/templatetags/admin_list.py | 2 +
django/contrib/admin/templatetags/admin_modify.py | 3 +-
django/contrib/admin/templatetags/admin_urls.py | 42 ++++++
django/contrib/admin/views/main.py | 1 +
docs/ref/contrib/admin/index.txt | 8 ++
docs/releases/1.6.txt | 15 ++
tests/admin_views/tests.py | 153 +++++++++++++++++++++
14 files changed, 317 insertions(+), 42 deletions(-)
(limited to 'docs')
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index be9671a76e..942385e1c6 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -13,6 +13,7 @@ from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_o
model_format_dict, NestedObjects, lookup_needs_distinct)
from django.contrib.admin import validation
from django.contrib.admin.templatetags.admin_static import static
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib import messages
from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied, ValidationError, FieldError
@@ -33,6 +34,7 @@ from django.utils.html import escape, escapejs
from django.utils.safestring import mark_safe
from django.utils import six
from django.utils.deprecation import RenameMethodsBase
+from django.utils.http import urlencode
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
@@ -393,6 +395,7 @@ class ModelAdmin(BaseModelAdmin):
save_as = False
save_on_top = False
paginator = Paginator
+ preserve_filters = True
inlines = []
# Custom templates (designed to be over-ridden in subclasses)
@@ -755,6 +758,27 @@ class ModelAdmin(BaseModelAdmin):
"""
return self.list_filter
+ def get_preserved_filters(self, request):
+ """
+ Returns the preserved filters querystring.
+ """
+
+ # FIXME: We can remove that getattr as soon as #20619 is fixed.
+ match = getattr(request, 'resolver_match', None)
+
+ if self.preserve_filters and match:
+ opts = self.model._meta
+ current_url = '%s:%s' % (match.namespace, match.url_name)
+ changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
+ if current_url == changelist_url:
+ preserved_filters = request.GET.urlencode()
+ else:
+ preserved_filters = request.GET.get('_changelist_filters')
+
+ if preserved_filters:
+ return urlencode({'_changelist_filters': preserved_filters})
+ return ''
+
def construct_change_message(self, request, form, formsets):
"""
Construct a change message from a changed object.
@@ -846,6 +870,8 @@ class ModelAdmin(BaseModelAdmin):
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
opts = self.model._meta
app_label = opts.app_label
+ preserved_filters = self.get_preserved_filters(request)
+ form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
context.update({
'add': add,
'change': change,
@@ -877,11 +903,19 @@ class ModelAdmin(BaseModelAdmin):
"""
opts = obj._meta
pk_value = obj._get_pk_val()
+ preserved_filters = self.get_preserved_filters(request)
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
# Here, we distinguish between different save types by checking for
# the presence of keys in request.POST.
- if "_continue" in request.POST:
+ if "_popup" in request.POST:
+ return HttpResponse(
+ ' '
+ '' % \
+ # escape() calls force_text.
+ (escape(pk_value), escapejs(obj)))
+
+ elif "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
if post_url_continue is None:
@@ -889,20 +923,16 @@ class ModelAdmin(BaseModelAdmin):
(opts.app_label, opts.model_name),
args=(pk_value,),
current_app=self.admin_site.name)
- if "_popup" in request.POST:
- post_url_continue += "?_popup=1"
+ post_url_continue = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url_continue)
return HttpResponseRedirect(post_url_continue)
- if "_popup" in request.POST:
- return HttpResponse(
- ' '
- '' % \
- # escape() calls force_text.
- (escape(pk_value), escapejs(obj)))
elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(request.path)
+ redirect_url = request.path
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
else:
msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
@@ -913,30 +943,36 @@ class ModelAdmin(BaseModelAdmin):
Determines the HttpResponse for the change_view stage.
"""
opts = self.model._meta
-
pk_value = obj._get_pk_val()
+ preserved_filters = self.get_preserved_filters(request)
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
if "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- if "_popup" in request.REQUEST:
- return HttpResponseRedirect(request.path + "?_popup=1")
- else:
- return HttpResponseRedirect(request.path)
+ redirect_url = request.path
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
elif "_saveasnew" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(reverse('admin:%s_%s_change' %
- (opts.app_label, opts.model_name),
- args=(pk_value,),
- current_app=self.admin_site.name))
+ redirect_url = reverse('admin:%s_%s_change' %
+ (opts.app_label, opts.model_name),
+ args=(pk_value,),
+ current_app=self.admin_site.name)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(reverse('admin:%s_%s_add' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name))
+ redirect_url = reverse('admin:%s_%s_add' %
+ (opts.app_label, opts.model_name),
+ current_app=self.admin_site.name)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
else:
msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
@@ -952,6 +988,8 @@ class ModelAdmin(BaseModelAdmin):
post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name),
current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
else:
post_url = reverse('admin:index',
current_app=self.admin_site.name)
@@ -963,10 +1001,13 @@ class ModelAdmin(BaseModelAdmin):
when editing an existing object.
"""
opts = self.model._meta
+
if self.has_change_permission(request, None):
post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name),
current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
else:
post_url = reverse('admin:index',
current_app=self.admin_site.name)
@@ -1122,6 +1163,7 @@ class ModelAdmin(BaseModelAdmin):
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True)
@@ -1214,6 +1256,7 @@ class ModelAdmin(BaseModelAdmin):
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
@@ -1357,11 +1400,13 @@ class ModelAdmin(BaseModelAdmin):
'cl': cl,
'media': media,
'has_add_permission': self.has_add_permission(request),
+ 'opts': cl.opts,
'app_label': app_label,
'action_form': action_form,
'actions_on_top': self.actions_on_top,
'actions_on_bottom': self.actions_on_bottom,
'actions_selection_counter': self.actions_selection_counter,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
@@ -1406,12 +1451,16 @@ class ModelAdmin(BaseModelAdmin):
'obj': force_text(obj_display)},
messages.SUCCESS)
- if not self.has_change_permission(request, None):
- return HttpResponseRedirect(reverse('admin:index',
- current_app=self.admin_site.name))
- return HttpResponseRedirect(reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name))
+ if self.has_change_permission(request, None):
+ post_url = reverse('admin:%s_%s_changelist' %
+ (opts.app_label, opts.model_name),
+ current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
+ else:
+ post_url = reverse('admin:index',
+ current_app=self.admin_site.name)
+ return HttpResponseRedirect(post_url)
object_name = force_text(opts.verbose_name)
@@ -1429,6 +1478,7 @@ class ModelAdmin(BaseModelAdmin):
"protected": protected,
"opts": opts,
"app_label": app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
@@ -1463,6 +1513,7 @@ class ModelAdmin(BaseModelAdmin):
'object': obj,
'app_label': app_label,
'opts': opts,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return TemplateResponse(request, self.object_history_template or [
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 4accf80c46..d4935ed781 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_modify %}
-{% load admin_urls %}
+{% load i18n admin_urls admin_static admin_modify %}
{% block extrahead %}{{ block.super }}
@@ -29,7 +28,10 @@
{% if change %}{% if not is_popup %}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 200b4cd74d..15583d1a8c 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_list %}
-{% load admin_urls %}
+{% load i18n admin_urls admin_static admin_list %}
{% block extrastyle %}
{{ block.super }}
@@ -54,7 +53,8 @@