Refs #15902 -- Deprecated storing user's language in the session.

This commit is contained in:
Claude Paroz 2016-08-14 22:42:49 +02:00 committed by Tim Graham
parent 76990cbbda
commit a8e2a9bac6
11 changed files with 94 additions and 57 deletions

View File

@ -7,7 +7,6 @@ from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare from django.utils.crypto import constant_time_compare
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import LANGUAGE_SESSION_KEY
from .signals import user_logged_in, user_logged_out, user_login_failed from .signals import user_logged_in, user_logged_out, user_login_failed
@ -143,15 +142,7 @@ def logout(request):
if not getattr(user, 'is_authenticated', True): if not getattr(user, 'is_authenticated', True):
user = None user = None
user_logged_out.send(sender=user.__class__, request=request, user=user) user_logged_out.send(sender=user.__class__, request=request, user=user)
# remember language choice saved to session
language = request.session.get(LANGUAGE_SESSION_KEY)
request.session.flush() request.session.flush()
if language is not None:
request.session[LANGUAGE_SESSION_KEY] = language
if hasattr(request, 'user'): if hasattr(request, 'user'):
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
request.user = AnonymousUser() request.user = AnonymousUser()

View File

@ -1,6 +1,7 @@
import base64 import base64
import logging import logging
import string import string
import warnings
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
@ -10,7 +11,9 @@ from django.utils import timezone
from django.utils.crypto import ( from django.utils.crypto import (
constant_time_compare, get_random_string, salted_hmac, constant_time_compare, get_random_string, salted_hmac,
) )
from django.utils.deprecation import RemovedInDjango40Warning
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import LANGUAGE_SESSION_KEY
# session_key should not be case sensitive because some backends can store it # session_key should not be case sensitive because some backends can store it
# on case insensitive file systems. # on case insensitive file systems.
@ -51,6 +54,13 @@ class SessionBase:
return key in self._session return key in self._session
def __getitem__(self, key): def __getitem__(self, key):
if key == LANGUAGE_SESSION_KEY:
warnings.warn(
'The user language will no longer be stored in '
'request.session in Django 4.0. Read it from '
'request.COOKIES[settings.LANGUAGE_COOKIE_NAME] instead.',
RemovedInDjango40Warning, stacklevel=2,
)
return self._session[key] return self._session[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):

View File

@ -15,7 +15,7 @@ from django.core.signals import setting_changed
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from . import LANGUAGE_SESSION_KEY, to_language, to_locale from . import to_language, to_locale
# Translations are cached in a dictionary for every language. # Translations are cached in a dictionary for every language.
# The active translations are stored by threadid to make them thread local. # The active translations are stored by threadid to make them thread local.
@ -456,14 +456,9 @@ def get_language_from_request(request, check_path=False):
if lang_code is not None: if lang_code is not None:
return lang_code return lang_code
supported_lang_codes = get_languages()
if hasattr(request, 'session'):
lang_code = request.session.get(LANGUAGE_SESSION_KEY)
if lang_code in supported_lang_codes and lang_code is not None and check_for_language(lang_code):
return lang_code
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
if lang_code is not None and lang_code in get_languages() and check_for_language(lang_code):
return lang_code
try: try:
return get_supported_language_variant(lang_code) return get_supported_language_variant(lang_code)

View File

@ -47,6 +47,8 @@ def set_language(request):
if next_trans != next: if next_trans != next:
response = HttpResponseRedirect(next_trans) response = HttpResponseRedirect(next_trans)
if hasattr(request, 'session'): if hasattr(request, 'session'):
# Storing the language in the session is deprecated.
# (RemovedInDjango40Warning)
request.session[LANGUAGE_SESSION_KEY] = lang_code request.session[LANGUAGE_SESSION_KEY] = lang_code
response.set_cookie( response.set_cookie(
settings.LANGUAGE_COOKIE_NAME, lang_code, settings.LANGUAGE_COOKIE_NAME, lang_code,

View File

@ -24,6 +24,9 @@ details on these changes.
``ugettext_noop()``, ``ungettext()``, and ``ungettext_lazy()`` will be ``ugettext_noop()``, ``ungettext()``, and ``ungettext_lazy()`` will be
removed. removed.
* ``django.views.i18n.set_language()`` will no longer set the user language in
``request.session`` (key ``django.utils.translation.LANGUAGE_SESSION_KEY``).
.. _deprecation-removed-in-3.1: .. _deprecation-removed-in-3.1:
3.1 3.1

View File

@ -1106,3 +1106,8 @@ functions without the ``u``.
Session key under which the active language for the current session is Session key under which the active language for the current session is
stored. stored.
.. deprecated:: 3.0
The language won't be stored in the session in Django 4.0. Use the
:setting:`LANGUAGE_COOKIE_NAME` cookie instead.

View File

@ -302,6 +302,11 @@ Miscellaneous
* ``ContentType.__str__()`` now includes the model's ``app_label`` to * ``ContentType.__str__()`` now includes the model's ``app_label`` to
disambiguate model's with the same name in different apps. disambiguate model's with the same name in different apps.
* Because accessing the language in the session rather than in the cookie is
deprecated, ``LocaleMiddleware`` no longer looks for the user's language in
the session and :func:`django.contrib.auth.logout` no longer preserves the
session's language after logout.
.. _deprecated-features-3.0: .. _deprecated-features-3.0:
Features deprecated in 3.0 Features deprecated in 3.0
@ -332,6 +337,11 @@ Miscellaneous
:func:`~django.utils.translation.ngettext`, and :func:`~django.utils.translation.ngettext`, and
:func:`~django.utils.translation.ngettext_lazy`. :func:`~django.utils.translation.ngettext_lazy`.
* To limit creation of sessions and hence favor some caching strategies,
:func:`django.views.i18n.set_language` will stop setting the user's language
in the session in Django 4.0. Since Django 2.1, the language is always stored
in the :setting:`LANGUAGE_COOKIE_NAME` cookie.
.. _removed-features-3.0: .. _removed-features-3.0:
Features removed in 3.0 Features removed in 3.0

View File

@ -1824,20 +1824,8 @@ You may want to set the active language for the current session explicitly. Perh
a user's language preference is retrieved from another system, for example. a user's language preference is retrieved from another system, for example.
You've already been introduced to :func:`django.utils.translation.activate()`. That You've already been introduced to :func:`django.utils.translation.activate()`. That
applies to the current thread only. To persist the language for the entire applies to the current thread only. To persist the language for the entire
session, also modify :data:`~django.utils.translation.LANGUAGE_SESSION_KEY` session in a cookie, set the :setting:`LANGUAGE_COOKIE_NAME` cookie on the
in the session:: response::
from django.utils import translation
user_language = 'fr'
translation.activate(user_language)
request.session[translation.LANGUAGE_SESSION_KEY] = user_language
You would typically want to use both: :func:`django.utils.translation.activate()`
will change the language for this thread, and modifying the session makes this
preference persist in future requests.
If you are not using sessions, the language will persist in a cookie, whose name
is configured in :setting:`LANGUAGE_COOKIE_NAME`. For example::
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
@ -1847,6 +1835,14 @@ is configured in :setting:`LANGUAGE_COOKIE_NAME`. For example::
response = HttpResponse(...) response = HttpResponse(...)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
You would typically want to use both: :func:`django.utils.translation.activate()`
changes the language for this thread, and setting the cookie makes this
preference persist in future requests.
.. versionchanged:: 3.0
In older versions, you could set the language in the current session.
Using translations outside views and templates Using translations outside views and templates
---------------------------------------------- ----------------------------------------------
@ -1980,9 +1976,6 @@ following this algorithm:
root URLconf. See :ref:`url-internationalization` for more information root URLconf. See :ref:`url-internationalization` for more information
about the language prefix and how to internationalize URL patterns. about the language prefix and how to internationalize URL patterns.
* Failing that, it looks for the :data:`~django.utils.translation.LANGUAGE_SESSION_KEY`
key in the current user's session.
* Failing that, it looks for a cookie. * Failing that, it looks for a cookie.
The name of the cookie used is set by the :setting:`LANGUAGE_COOKIE_NAME` The name of the cookie used is set by the :setting:`LANGUAGE_COOKIE_NAME`

View File

@ -31,7 +31,6 @@ from django.test import Client, TestCase, override_settings
from django.test.client import RedirectCycleError from django.test.client import RedirectCycleError
from django.urls import NoReverseMatch, reverse, reverse_lazy from django.urls import NoReverseMatch, reverse, reverse_lazy
from django.utils.http import urlsafe_base64_encode from django.utils.http import urlsafe_base64_encode
from django.utils.translation import LANGUAGE_SESSION_KEY
from .client import PasswordResetConfirmClient from .client import PasswordResetConfirmClient
from .models import CustomUser, UUIDUser from .models import CustomUser, UUIDUser
@ -1075,16 +1074,12 @@ class LogoutTest(AuthViewsTestCase):
self.confirm_logged_out() self.confirm_logged_out()
def test_logout_preserve_language(self): def test_logout_preserve_language(self):
"""Language stored in session is preserved after logout""" """Language is preserved after logout."""
# Create a new session with language self.login()
engine = import_module(settings.SESSION_ENGINE) self.client.post('/setlang/', {'language': 'pl'})
session = engine.SessionStore() self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, 'pl')
session[LANGUAGE_SESSION_KEY] = 'pl'
session.save()
self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
self.client.get('/logout/') self.client.get('/logout/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'pl') self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, 'pl')
@override_settings(LOGOUT_REDIRECT_URL='/custom/') @override_settings(LOGOUT_REDIRECT_URL='/custom/')
def test_logout_redirect_url_setting(self): def test_logout_redirect_url_setting(self):

View File

@ -9,6 +9,7 @@ from django.shortcuts import render
from django.template import RequestContext, Template from django.template import RequestContext, Template
from django.urls import path, re_path, reverse_lazy from django.urls import path, re_path, reverse_lazy
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.i18n import set_language
class CustomRequestAuthenticationForm(AuthenticationForm): class CustomRequestAuthenticationForm(AuthenticationForm):
@ -148,6 +149,7 @@ urlpatterns = auth_urlpatterns + [
path('permission_required_exception/', permission_required_exception), path('permission_required_exception/', permission_required_exception),
path('login_and_permission_required_exception/', login_and_permission_required_exception), path('login_and_permission_required_exception/', login_and_permission_required_exception),
path('setlang/', set_language, name='set_language'),
# This line is only required to render the password reset with is_admin=True # This line is only required to render the password reset with is_admin=True
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]

View File

@ -4,11 +4,12 @@ from os import path
from django.conf import settings from django.conf import settings
from django.test import ( from django.test import (
RequestFactory, SimpleTestCase, TestCase, modify_settings, RequestFactory, SimpleTestCase, TestCase, ignore_warnings, modify_settings,
override_settings, override_settings,
) )
from django.test.selenium import SeleniumTestCase from django.test.selenium import SeleniumTestCase
from django.urls import reverse from django.urls import reverse
from django.utils.deprecation import RemovedInDjango40Warning
from django.utils.translation import ( from django.utils.translation import (
LANGUAGE_SESSION_KEY, get_language, override, LANGUAGE_SESSION_KEY, get_language, override,
) )
@ -36,6 +37,7 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code, 'next': '/'} post_data = {'language': lang_code, 'next': '/'}
response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i_should_not_be_used/') response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i_should_not_be_used/')
self.assertRedirects(response, '/') self.assertRedirects(response, '/')
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
# The language is set in a cookie. # The language is set in a cookie.
language_cookie = self.client.cookies[settings.LANGUAGE_COOKIE_NAME] language_cookie = self.client.cookies[settings.LANGUAGE_COOKIE_NAME]
@ -53,6 +55,8 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code, 'next': '//unsafe/redirection/'} post_data = {'language': lang_code, 'next': '//unsafe/redirection/'}
response = self.client.post('/i18n/setlang/', data=post_data) response = self.client.post('/i18n/setlang/', data=post_data)
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_http_next(self): def test_setlang_http_next(self):
@ -66,10 +70,14 @@ class SetLanguageTests(TestCase):
# Insecure URL in POST data. # Insecure URL in POST data.
response = self.client.post('/i18n/setlang/', data=post_data, secure=True) response = self.client.post('/i18n/setlang/', data=post_data, secure=True)
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
# Insecure URL in HTTP referer. # Insecure URL in HTTP referer.
response = self.client.post('/i18n/setlang/', secure=True, HTTP_REFERER=non_https_next_url) response = self.client.post('/i18n/setlang/', secure=True, HTTP_REFERER=non_https_next_url)
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_redirect_to_referer(self): def test_setlang_redirect_to_referer(self):
@ -81,6 +89,8 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code} post_data = {'language': lang_code}
response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i18n/') response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i18n/')
self.assertRedirects(response, '/i18n/', fetch_redirect_response=False) self.assertRedirects(response, '/i18n/', fetch_redirect_response=False)
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_default_redirect(self): def test_setlang_default_redirect(self):
@ -92,6 +102,8 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code} post_data = {'language': lang_code}
response = self.client.post('/i18n/setlang/', post_data) response = self.client.post('/i18n/setlang/', post_data)
self.assertRedirects(response, '/') self.assertRedirects(response, '/')
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self): def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self):
@ -102,6 +114,8 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code, 'next': '/'} post_data = {'language': lang_code, 'next': '/'}
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertRedirects(response, '/') self.assertRedirects(response, '/')
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self): def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self):
@ -114,6 +128,8 @@ class SetLanguageTests(TestCase):
headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
response = self.client.post('/i18n/setlang/', post_data, **headers) response = self.client.post('/i18n/setlang/', post_data, **headers)
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self): def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self):
@ -124,6 +140,8 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code} post_data = {'language': lang_code}
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_unsafe_next_for_ajax(self): def test_setlang_unsafe_next_for_ajax(self):
@ -134,7 +152,16 @@ class SetLanguageTests(TestCase):
post_data = {'language': lang_code, 'next': '//unsafe/redirection/'} post_data = {'language': lang_code, 'next': '//unsafe/redirection/'}
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
def test_session_langauge_deprecation(self):
msg = (
'The user language will no longer be stored in request.session '
'in Django 4.0. Read it from '
'request.COOKIES[settings.LANGUAGE_COOKIE_NAME] instead.'
)
with self.assertRaisesMessage(RemovedInDjango40Warning, msg):
self.client.session[LANGUAGE_SESSION_KEY]
def test_setlang_reversal(self): def test_setlang_reversal(self):
self.assertEqual(reverse('set_language'), '/i18n/setlang/') self.assertEqual(reverse('set_language'), '/i18n/setlang/')
@ -168,6 +195,8 @@ class SetLanguageTests(TestCase):
encoded_url = '/test-setlang/%C3%A4/' # (%C3%A4 decodes to ä) encoded_url = '/test-setlang/%C3%A4/' # (%C3%A4 decodes to ä)
response = self.client.post('/i18n/setlang/', {'language': lang_code}, HTTP_REFERER=encoded_url) response = self.client.post('/i18n/setlang/', {'language': lang_code}, HTTP_REFERER=encoded_url)
self.assertRedirects(response, encoded_url, fetch_redirect_response=False) self.assertRedirects(response, encoded_url, fetch_redirect_response=False)
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, lang_code)
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
@modify_settings(MIDDLEWARE={ @modify_settings(MIDDLEWARE={
@ -178,6 +207,8 @@ class SetLanguageTests(TestCase):
'/i18n/setlang/', data={'language': 'nl'}, '/i18n/setlang/', data={'language': 'nl'},
follow=True, HTTP_REFERER='/en/translated/' follow=True, HTTP_REFERER='/en/translated/'
) )
self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, 'nl')
with ignore_warnings(category=RemovedInDjango40Warning):
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'nl') self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'nl')
self.assertRedirects(response, '/nl/vertaald/') self.assertRedirects(response, '/nl/vertaald/')
# And reverse # And reverse