Repository: learningequality/kolibri

Issue:
Django language cookie persistently overrides device language setting
<!--
Instructions:
 * Fill out the sections below, replace …'s with information about your issue
 * Use the 'preview' function above this text box to verify formatting before submitting
-->

### Observed behavior
<!--
Description of the behavior that was observed, including screenshots or other references when applicable
-->

When a super admin sets the device language - for example, to Spanish - and a user later uses the language selection modal to personalize to different language, for example, French - then the browser that the user used to select French will not automatically revert to using the device language.

The translation logic prioritizes the language code in the URL first, then sessions and cookies, then the device language setting. However, we are not clearing that cookie regularly and a user must enter an Incognito browser to access Kolibri in the device settings language.

### Expected behavior
<!--
Description of what behavior was expected but did not occur
-->

At some point, the device settings language ought to take priority. Using a user's language of choice that they personally set in the browser for an extended period of time makes sense - however - it ought to be safe to assume that the device admin has set a language that best serves their and their learners' needs for the most part.

Ideally, we would have a timed refresh where a browser's cookie expires and the browser defaults to the device language.

### User-facing consequences
<!--
Implications and real-world consequences for learners, coaches, admins, and other users of the application
-->

Users may need to manually adjust their language settings  between sessions, particularly in a shared context.



Discussion:
Can probably resolve this by explicitly setting https://docs.djangoproject.com/en/1.11/ref/settings/#language-cookie-age to a specific/somewhat low value.
I added `LANGUAGE_COOKIE_AGE = 60` to the base.py conf file and the cookie expires as expected.

Now that I'm working with it, I'm thinking that we might not need the Django language cookie at all.

If we don't set any cookie at all - the language code is still stored on the user's session. So, if you select a language different than the device language, you will continuously see Kolibri in that language until your session is killed by the server or the session cookie is cleared from your browser.

Even if your session is killed or your session cookie is cleared, you will continue seeing Kolibri in your chosen language because we prioritize language code in the URL as the primary indicator for what language to direct to. The session expiring will only redirect you to the device language when you go to Kolibri root.

So - perhaps - it would be worth considering if we need to store the language on _either_ the cookie or the session but not both?
That seems right to me.

The other thing to consider though is that the user's language selection should really be retained across sessions, so it ought to be stored as an attribute on the user model.


It think it would be reasonable not to use the Django language cookie - however, I think, as @indirectlylit suggests, we should store it as extra information about the user, so maybe we can do both things in tandem?

Resolving patch:
diff --git a/kolibri/core/device/translation.py b/kolibri/core/device/translation.py
--- a/kolibri/core/device/translation.py
+++ b/kolibri/core/device/translation.py
@@ -90,13 +90,6 @@ def get_language_from_request_and_is_from_path(request):  # noqa complexity-16
         ):
             return lang_code, False
 
-    lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
-
-    try:
-        return get_supported_language_variant(lang_code), False
-    except LookupError:
-        pass
-
     device_language = get_device_language()
 
     if device_language is not None:
diff --git a/kolibri/core/views.py b/kolibri/core/views.py
--- a/kolibri/core/views.py
+++ b/kolibri/core/views.py
@@ -1,4 +1,3 @@
-from django.conf import settings
 from django.contrib.auth import logout
 from django.core.urlresolvers import reverse
 from django.http import Http404
@@ -57,14 +56,6 @@ def set_language(request):
         response = HttpResponse(next_path)
         if hasattr(request, "session"):
             request.session[LANGUAGE_SESSION_KEY] = lang_code
-        # Always set cookie
-        response.set_cookie(
-            settings.LANGUAGE_COOKIE_NAME,
-            lang_code,
-            max_age=settings.LANGUAGE_COOKIE_AGE,
-            path=settings.LANGUAGE_COOKIE_PATH,
-            domain=settings.LANGUAGE_COOKIE_DOMAIN,
-        )
     else:
         lang_code = (
             get_device_language()
@@ -87,7 +78,6 @@ def set_language(request):
         response = HttpResponse(next_path)
         if hasattr(request, "session"):
             request.session.pop(LANGUAGE_SESSION_KEY, "")
-        response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
     return response
 
 
diff --git a/kolibri/deployment/default/settings/base.py b/kolibri/deployment/default/settings/base.py
--- a/kolibri/deployment/default/settings/base.py
+++ b/kolibri/deployment/default/settings/base.py
@@ -251,11 +251,7 @@
 # Ensure that our CSRF cookie does not collide with other CSRF cookies
 # set by other Django apps served from the same domain.
 CSRF_COOKIE_PATH = path_prefix
-
-# https://docs.djangoproject.com/en/1.11/ref/settings/#language-cookie-path
-# Ensure that our language cookie does not collide with other language
-# cookies set by other Django apps served from the same domain.
-LANGUAGE_COOKIE_PATH = path_prefix
+CSRF_COOKIE_NAME = "kolibri_csrftoken"
 
 # https://docs.djangoproject.com/en/1.11/ref/settings/#session-cookie-path
 # Ensure that our session cookie does not collidge with other session cookies


Test patch:
diff --git a/kolibri/core/device/test/test_locale_middleware.py b/kolibri/core/device/test/test_locale_middleware.py
--- a/kolibri/core/device/test/test_locale_middleware.py
+++ b/kolibri/core/device/test/test_locale_middleware.py
@@ -3,7 +3,6 @@
 import os
 
 from django.conf import settings
-from django.http.cookie import SimpleCookie
 from django.test import override_settings
 from django.test import TestCase
 from django.urls import clear_url_caches
@@ -128,11 +127,6 @@ def test_fr_fr_prefixed_redirect_session(self):
         response = self.client.get(get_url("prefixed/"), follow=True)
         self.assertRedirects(response, get_url("fr-fr/prefixed/"), 302)
 
-    def test_fr_fr_prefixed_redirect_cookie(self):
-        self.client.cookies = SimpleCookie({settings.LANGUAGE_COOKIE_NAME: "fr-fr"})
-        response = self.client.get(get_url("prefixed/"), follow=True)
-        self.assertRedirects(response, get_url("fr-fr/prefixed/"), 302)
-
     def test_fr_fr_prefixed_redirect_device_setting(self):
         with patch(
             "kolibri.core.device.translation.get_device_language", return_value="fr-fr"
diff --git a/kolibri/core/test/test_key_urls.py b/kolibri/core/test/test_key_urls.py
--- a/kolibri/core/test/test_key_urls.py
+++ b/kolibri/core/test/test_key_urls.py
@@ -184,16 +184,6 @@ def test_default_language_without_namespaced_logout(self):
         response = self.client.get("/logout")
         self.assertTrue(get_settings_language() in response.url)
 
-    def test_persistent_cookie_language_setting_on_logout(self):
-        # Test when set on a cookie.
-        from django.http.cookie import SimpleCookie
-        from django.conf import settings
-
-        self.client.login(**self.credentials)
-        self.client.cookies = SimpleCookie({settings.LANGUAGE_COOKIE_NAME: "fr-fr"})
-        response = self.client.post("/logout")
-        self.assertTrue("fr-fr" in response.url)
-
     def test_persistent_session_language_setting_on_logout(self):
         # Test when set on a session.
         from django.utils.translation import LANGUAGE_SESSION_KEY
diff --git a/kolibri/core/test/test_setlanguage.py b/kolibri/core/test/test_setlanguage.py
--- a/kolibri/core/test/test_setlanguage.py
+++ b/kolibri/core/test/test_setlanguage.py
@@ -162,43 +162,3 @@ def test_setlang_for_ajax(self):
             translate_url(reverse("kolibri:core:redirect_user"), lang_code),
         )
         self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
-
-    def test_setlang_cookie(self):
-        # we force saving language to a cookie rather than a session
-        # by excluding session middleware and those which do require it
-        test_settings = {
-            "MIDDLEWARE": ["django.middleware.common.CommonMiddleware"],
-            "LANGUAGE_COOKIE_NAME": "mylanguage",
-            "LANGUAGE_COOKIE_AGE": 3600 * 7 * 2,
-            "LANGUAGE_COOKIE_DOMAIN": ".example.com",
-            "LANGUAGE_COOKIE_PATH": "/test/",
-        }
-        with self.settings(**test_settings):
-            post_data = dict(language="pl")
-            response = self.client.post(
-                reverse("kolibri:core:set_language"), data=post_data
-            )
-            language_cookie = response.cookies.get("mylanguage")
-            self.assertEqual(language_cookie.value, "pl")
-            self.assertEqual(language_cookie["domain"], ".example.com")
-            self.assertEqual(language_cookie["path"], "/test/")
-            self.assertEqual(language_cookie["max-age"], 3600 * 7 * 2)
-
-    def test_setlang_cookie_null(self):
-        # we force saving language to a cookie rather than a session
-        # by excluding session middleware and those which do require it
-        test_settings = {
-            "MIDDLEWARE": ["django.middleware.common.CommonMiddleware"],
-            "LANGUAGE_COOKIE_NAME": "mylanguage",
-            "LANGUAGE_COOKIE_AGE": 3600 * 7 * 2,
-            "LANGUAGE_COOKIE_DOMAIN": ".example.com",
-            "LANGUAGE_COOKIE_PATH": "/test/",
-        }
-        with self.settings(**test_settings):
-            post_data = dict(language=None)
-            response = self.client.post(
-                reverse("kolibri:core:set_language"), data=post_data
-            )
-            language_cookie = response.cookies.get("mylanguage")
-            self.assertTrue(language_cookie)
-            self.assertEqual(language_cookie.value, "")