Repository: e-valuation/EvaP

Issue:
Lost in translation: Language preference dropped when browser is closed
The language preference is lost after I restart my browser. This happens on both Firefox and Chromium.

1. log in
2. set language to German (my system language is English)
3. close and reopen browser
4. go to evap, now the site is English again

@He3lixxx reproduced and said that the django cookie for language is discarded after the session ends. It gets restored if you have the "restore session when opening firefox" setting enabled (edit: when coming back to the site (still logged in) with a new session, the site is served in English although the database knows the language for the user is German)

We have `set_or_get_language` that runs on `@receiver(user_logged_in)`, but when I open the log in site, the language is set to English anyways and stays that way after I logged in.


Discussion:

Resolving patch:
diff --git a/evap/evaluation/tools.py b/evap/evaluation/tools.py
--- a/evap/evaluation/tools.py
+++ b/evap/evaluation/tools.py
@@ -4,11 +4,8 @@
 
 import xlwt
 from django.conf import settings
-from django.contrib.auth import user_logged_in
-from django.dispatch import receiver
 from django.http import HttpResponse
-from django.utils import translation
-from django.utils.translation import LANGUAGE_SESSION_KEY, get_language
+from django.utils.translation import get_language
 
 
 def is_prefetched(instance, attribute_name):
@@ -38,16 +35,6 @@ def vote_end_datetime(vote_end_date):
     return date_to_datetime(vote_end_date) + datetime.timedelta(hours=24 + settings.EVALUATION_END_OFFSET_HOURS)
 
 
-@receiver(user_logged_in)
-def set_or_get_language(user, request, **_kwargs):
-    if user.language:
-        translation.activate(user.language)
-    else:
-        user.language = get_language()
-        user.save()
-    request.session[LANGUAGE_SESSION_KEY] = user.language
-
-
 def get_parameter_from_url_or_session(request, parameter, default=False):
     result = request.GET.get(parameter, None)
     if result is None:  # if no parameter is given take session value
diff --git a/evap/middleware.py b/evap/middleware.py
--- a/evap/middleware.py
+++ b/evap/middleware.py
@@ -1,4 +1,6 @@
+from django.conf import settings
 from django.contrib.auth.views import redirect_to_login
+from django.utils import translation
 
 
 class RequireLoginMiddleware:
@@ -26,3 +28,23 @@ def process_view(request, view_func, _view_args, _view_kwargs):
 def no_login_required(func):
     func.no_login_required = True
     return func
+
+
+def user_language_middleware(get_response):
+    def middleware(request):
+        if not (request.user and request.user.is_authenticated):
+            return get_response(request)
+        if request.user.language == translation.get_language():
+            return get_response(request)
+
+        if request.user.language:
+            translation.activate(request.user.language)
+        else:
+            request.user.language = translation.get_language()
+            request.user.save()
+        lang = request.user.language
+        response = get_response(request)
+        response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
+        return response
+
+    return middleware
diff --git a/evap/settings.py b/evap/settings.py
--- a/evap/settings.py
+++ b/evap/settings.py
@@ -218,7 +218,6 @@
     "django.middleware.security.SecurityMiddleware",
     "django.contrib.sessions.middleware.SessionMiddleware",
     # LocaleMiddleware should be here according to https://docs.djangoproject.com/en/2.2/topics/i18n/translation/#how-django-discovers-language-preference
-    # Furthermore, set_or_get_language (happens on login) uses the active language, so LocaleMiddleware should be before AuthenticationMiddleware
     "django.middleware.locale.LocaleMiddleware",
     "django.middleware.common.CommonMiddleware",
     "django.middleware.csrf.CsrfViewMiddleware",
@@ -227,6 +226,7 @@
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "mozilla_django_oidc.middleware.SessionRefresh",
     "evap.middleware.RequireLoginMiddleware",
+    "evap.middleware.user_language_middleware",
     "evap.staff.staff_mode.staff_mode_middleware",
     "evap.evaluation.middleware.LoggingRequestMiddleware",
 ]


Test patch:
diff --git a/evap/results/tests/test_views.py b/evap/results/tests/test_views.py
--- a/evap/results/tests/test_views.py
+++ b/evap/results/tests/test_views.py
@@ -93,7 +93,9 @@ def test_order(self):
         page = self.app.get(self.url, user=student).body.decode()
         self.assertLess(page.index(evaluation1.name_en), page.index(evaluation2.name_en))
 
-        page = self.app.get(self.url, user=student, extra_environ={"HTTP_ACCEPT_LANGUAGE": "de"}).body.decode()
+        student.language = "de"
+        student.save()
+        page = self.app.get(self.url, user=student).body.decode()
         self.assertGreater(page.index(evaluation1.name_de), page.index(evaluation2.name_de))
 
     # using LocMemCache so the cache queries don't show up in the query count that's measured here