Browse Source

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

Claude Paroz 8 years ago
parent
commit
a8e2a9bac6

+ 0 - 9
django/contrib/auth/__init__.py

@@ -7,7 +7,6 @@ from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 from django.middleware.csrf import rotate_token
 from django.utils.crypto import constant_time_compare
 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
 
@@ -143,15 +142,7 @@ def logout(request):
     if not getattr(user, 'is_authenticated', True):
         user = None
     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()
-
-    if language is not None:
-        request.session[LANGUAGE_SESSION_KEY] = language
-
     if hasattr(request, 'user'):
         from django.contrib.auth.models import AnonymousUser
         request.user = AnonymousUser()

+ 10 - 0
django/contrib/sessions/backends/base.py

@@ -1,6 +1,7 @@
 import base64
 import logging
 import string
+import warnings
 from datetime import datetime, timedelta
 
 from django.conf import settings
@@ -10,7 +11,9 @@ from django.utils import timezone
 from django.utils.crypto import (
     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.translation import LANGUAGE_SESSION_KEY
 
 # session_key should not be case sensitive because some backends can store it
 # on case insensitive file systems.
@@ -51,6 +54,13 @@ class SessionBase:
         return key in self._session
 
     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]
 
     def __setitem__(self, key, value):

+ 3 - 8
django/utils/translation/trans_real.py

@@ -15,7 +15,7 @@ from django.core.signals import setting_changed
 from django.dispatch import receiver
 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.
 # 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:
             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)
+    if lang_code is not None and lang_code in get_languages() and check_for_language(lang_code):
+        return lang_code
 
     try:
         return get_supported_language_variant(lang_code)

+ 2 - 0
django/views/i18n.py

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

+ 3 - 0
docs/internals/deprecation.txt

@@ -24,6 +24,9 @@ details on these changes.
   ``ugettext_noop()``, ``ungettext()``, and ``ungettext_lazy()`` will be
   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:
 
 3.1

+ 5 - 0
docs/ref/utils.txt

@@ -1106,3 +1106,8 @@ functions without the ``u``.
 
     Session key under which the active language for the current session is
     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.

+ 10 - 0
docs/releases/3.0.txt

@@ -302,6 +302,11 @@ Miscellaneous
 * ``ContentType.__str__()`` now includes the model's ``app_label`` to
   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:
 
 Features deprecated in 3.0
@@ -332,6 +337,11 @@ Miscellaneous
   :func:`~django.utils.translation.ngettext`, and
   :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:
 
 Features removed in 3.0

+ 9 - 16
docs/topics/i18n/translation.txt

@@ -1824,28 +1824,24 @@ 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.
 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
-session, also modify :data:`~django.utils.translation.LANGUAGE_SESSION_KEY`
-in the session::
+session in a cookie, set the :setting:`LANGUAGE_COOKIE_NAME` cookie on the
+response::
 
+    from django.conf import settings
+    from django.http import HttpResponse
     from django.utils import translation
     user_language = 'fr'
     translation.activate(user_language)
-    request.session[translation.LANGUAGE_SESSION_KEY] = user_language
+    response = HttpResponse(...)
+    response.set_cookie(settings.LANGUAGE_COOKIE_NAME, 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
+changes the language for this thread, and setting the cookie 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::
+.. versionchanged:: 3.0
 
-    from django.conf import settings
-    from django.http import HttpResponse
-    from django.utils import translation
-    user_language = 'fr'
-    translation.activate(user_language)
-    response = HttpResponse(...)
-    response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
+    In older versions, you could set the language in the current session.
 
 Using translations outside views and templates
 ----------------------------------------------
@@ -1980,9 +1976,6 @@ following this algorithm:
   root URLconf. See :ref:`url-internationalization` for more information
   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.
 
   The name of the cookie used is set by the :setting:`LANGUAGE_COOKIE_NAME`

+ 5 - 10
tests/auth_tests/test_views.py

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

+ 2 - 0
tests/auth_tests/urls.py

@@ -9,6 +9,7 @@ from django.shortcuts import render
 from django.template import RequestContext, Template
 from django.urls import path, re_path, reverse_lazy
 from django.views.decorators.cache import never_cache
+from django.views.i18n import set_language
 
 
 class CustomRequestAuthenticationForm(AuthenticationForm):
@@ -148,6 +149,7 @@ urlpatterns = auth_urlpatterns + [
     path('permission_required_exception/', 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
     path('admin/', admin.site.urls),
 ]

+ 44 - 13
tests/view_tests/tests/test_i18n.py

@@ -4,11 +4,12 @@ from os import path
 
 from django.conf import settings
 from django.test import (
-    RequestFactory, SimpleTestCase, TestCase, modify_settings,
+    RequestFactory, SimpleTestCase, TestCase, ignore_warnings, modify_settings,
     override_settings,
 )
 from django.test.selenium import SeleniumTestCase
 from django.urls import reverse
+from django.utils.deprecation import RemovedInDjango40Warning
 from django.utils.translation import (
     LANGUAGE_SESSION_KEY, get_language, override,
 )
@@ -36,7 +37,8 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code, 'next': '/'}
         response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i_should_not_be_used/')
         self.assertRedirects(response, '/')
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        with ignore_warnings(category=RemovedInDjango40Warning):
+            self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
         # The language is set in a cookie.
         language_cookie = self.client.cookies[settings.LANGUAGE_COOKIE_NAME]
         self.assertEqual(language_cookie.value, lang_code)
@@ -53,7 +55,9 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code, 'next': '//unsafe/redirection/'}
         response = self.client.post('/i18n/setlang/', data=post_data)
         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)
+        with ignore_warnings(category=RemovedInDjango40Warning):
+            self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
 
     def test_setlang_http_next(self):
         """
@@ -66,11 +70,15 @@ class SetLanguageTests(TestCase):
         # Insecure URL in POST data.
         response = self.client.post('/i18n/setlang/', data=post_data, secure=True)
         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)
+        with ignore_warnings(category=RemovedInDjango40Warning):
+            self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
         # Insecure URL in HTTP referer.
         response = self.client.post('/i18n/setlang/', secure=True, HTTP_REFERER=non_https_next_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)
+        with ignore_warnings(category=RemovedInDjango40Warning):
+            self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
 
     def test_setlang_redirect_to_referer(self):
         """
@@ -81,7 +89,9 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code}
         response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i18n/')
         self.assertRedirects(response, '/i18n/', fetch_redirect_response=False)
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        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)
 
     def test_setlang_default_redirect(self):
         """
@@ -92,7 +102,9 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code}
         response = self.client.post('/i18n/setlang/', post_data)
         self.assertRedirects(response, '/')
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        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)
 
     def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self):
         """
@@ -102,7 +114,9 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code, 'next': '/'}
         response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
         self.assertRedirects(response, '/')
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        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)
 
     def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self):
         """
@@ -114,7 +128,9 @@ class SetLanguageTests(TestCase):
         headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
         response = self.client.post('/i18n/setlang/', post_data, **headers)
         self.assertEqual(response.status_code, 204)
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        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)
 
     def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self):
         """
@@ -124,7 +140,9 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code}
         response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
         self.assertEqual(response.status_code, 204)
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        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)
 
     def test_setlang_unsafe_next_for_ajax(self):
         """
@@ -134,7 +152,16 @@ class SetLanguageTests(TestCase):
         post_data = {'language': lang_code, 'next': '//unsafe/redirection/'}
         response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
         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):
         self.assertEqual(reverse('set_language'), '/i18n/setlang/')
@@ -168,7 +195,9 @@ class SetLanguageTests(TestCase):
         encoded_url = '/test-setlang/%C3%A4/'  # (%C3%A4 decodes to ä)
         response = self.client.post('/i18n/setlang/', {'language': lang_code}, HTTP_REFERER=encoded_url)
         self.assertRedirects(response, encoded_url, fetch_redirect_response=False)
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+        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)
 
     @modify_settings(MIDDLEWARE={
         'append': 'django.middleware.locale.LocaleMiddleware',
@@ -178,7 +207,9 @@ class SetLanguageTests(TestCase):
             '/i18n/setlang/', data={'language': 'nl'},
             follow=True, HTTP_REFERER='/en/translated/'
         )
-        self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'nl')
+        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.assertRedirects(response, '/nl/vertaald/')
         # And reverse
         response = self.client.post(