diff --git a/contentcuration/contentcuration/frontend/shared/languageSwitcher/mixin.js b/contentcuration/contentcuration/frontend/shared/languageSwitcher/mixin.js
index 8b75b03191..682c676eae 100644
--- a/contentcuration/contentcuration/frontend/shared/languageSwitcher/mixin.js
+++ b/contentcuration/contentcuration/frontend/shared/languageSwitcher/mixin.js
@@ -1,4 +1,5 @@
import { availableLanguages, currentLanguage, sortLanguages } from '../i18n';
+
import client from 'shared/client';
export default {
diff --git a/contentcuration/contentcuration/frontend/shared/views/AppBar.vue b/contentcuration/contentcuration/frontend/shared/views/AppBar.vue
index e018472528..f437e06e51 100644
--- a/contentcuration/contentcuration/frontend/shared/views/AppBar.vue
+++ b/contentcuration/contentcuration/frontend/shared/views/AppBar.vue
@@ -75,9 +75,30 @@
-
- {{ $tr('logIn') }}
-
+
+
+
diff --git a/contentcuration/contentcuration/frontend/shared/views/MainNavigationDrawer.vue b/contentcuration/contentcuration/frontend/shared/views/MainNavigationDrawer.vue
index 32f4a95ed6..c5065c49ac 100644
--- a/contentcuration/contentcuration/frontend/shared/views/MainNavigationDrawer.vue
+++ b/contentcuration/contentcuration/frontend/shared/views/MainNavigationDrawer.vue
@@ -32,6 +32,7 @@
{{ $tr('administrationLink') }}
+
@@ -41,6 +42,14 @@
{{ $tr('settingsLink') }}
+
+
+ language
+
+
+
+
+
open_in_new
@@ -73,8 +82,15 @@
/>
+
+
+
@@ -84,11 +100,13 @@
import { mapActions, mapState } from 'vuex';
import KolibriLogo from './KolibriLogo';
+ import LanguageSwitcherModal from 'shared/languageSwitcher/LanguageSwitcherModal';
export default {
name: 'MainNavigationDrawer',
components: {
KolibriLogo,
+ LanguageSwitcherModal,
},
props: {
value: {
@@ -96,6 +114,11 @@
default: false,
},
},
+ data() {
+ return {
+ showLanguageModal: false,
+ };
+ },
computed: {
...mapState({
user: state => state.session.currentUser,
@@ -131,6 +154,7 @@
channelsLink: 'Channels',
administrationLink: 'Administration',
settingsLink: 'Settings',
+ changeLanguage: 'Change language',
helpLink: 'Help and support',
logoutLink: 'Sign out',
copyright: '© {year} Learning Equality',
diff --git a/contentcuration/contentcuration/settings.py b/contentcuration/contentcuration/settings.py
index bd857d651b..1c7d962d9f 100644
--- a/contentcuration/contentcuration/settings.py
+++ b/contentcuration/contentcuration/settings.py
@@ -427,3 +427,5 @@ def gettext(s):
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
+
+LANGUAGE_COOKIE_AGE = 3600 * 24 * 14
diff --git a/contentcuration/contentcuration/templates/pwa/service_worker.js b/contentcuration/contentcuration/templates/pwa/service_worker.js
index 583a43bdf2..f69abd79bf 100644
--- a/contentcuration/contentcuration/templates/pwa/service_worker.js
+++ b/contentcuration/contentcuration/templates/pwa/service_worker.js
@@ -1,6 +1,5 @@
{% load js_reverse %}
-{% js_reverse_inline %}
{% autoescape off %}
{{ webpack_service_worker }}
diff --git a/contentcuration/contentcuration/tests/test_setlanguage.py b/contentcuration/contentcuration/tests/test_setlanguage.py
new file mode 100644
index 0000000000..941db98f5d
--- /dev/null
+++ b/contentcuration/contentcuration/tests/test_setlanguage.py
@@ -0,0 +1,150 @@
+import json
+
+from django.conf import settings
+from django.http import HttpResponseNotAllowed
+from django.test import override_settings
+from django.test import TestCase
+from django.urls import reverse
+from django.urls import translate_url
+from django.utils.translation import get_language
+from django.utils.translation import LANGUAGE_SESSION_KEY
+
+
+@override_settings(LANGUAGE_CODE="en")
+class I18NTests(TestCase):
+ """
+ Tests set_language view in kolibri/core/views.py
+ Copied from https://github.com/django/django/blob/stable/1.11.x/tests/view_tests/tests/test_i18n.py
+ """
+
+ def set_post_data(self, lang_code, next_url=""):
+ post_data = {
+ "language": lang_code,
+ "next": next_url,
+ }
+ return json.dumps(post_data)
+
+ def _get_inactive_language_code(self):
+ """Return language code for a language which is not activated."""
+ current_language = get_language()
+ return [
+ code for code, name in settings.LANGUAGES if not code == current_language
+ ][0]
+
+ def test_setlang(self):
+ """
+ The set_language view can be used to change the session language.
+ """
+ lang_code = self._get_inactive_language_code()
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), lang_code),
+ )
+ self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+
+ def test_setlang_next_valid(self):
+ """
+ The set_language view can be used to change the session language.
+ The user is redirected to the "next" argument.
+ """
+ lang_code = self._get_inactive_language_code()
+ next_url = reverse("channels")
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code, next_url), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("channels"), lang_code),
+ )
+ self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+
+ def test_setlang_next_invalid(self):
+ """
+ The set_language view can be used to change the session language.
+ The user is redirected to base redirect if the "next" argument is invalid.
+ """
+ lang_code = self._get_inactive_language_code()
+ next_url = "/not/a/real/url"
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code, next_url), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), lang_code),
+ )
+ self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+
+ def test_setlang_null(self):
+ """
+ Test language code set to null which shoul direct to default language "en"
+ """
+ lang_code = self._get_inactive_language_code()
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), lang_code),
+ )
+ self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+ lang_code = None
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), "en"),
+ )
+ self.assertFalse(LANGUAGE_SESSION_KEY in self.client.session)
+
+ def test_setlang_null_next_valid(self):
+ """
+ The set_language view can be used to change the session language.
+ The user is redirected to the "next" argument.
+ """
+ lang_code = self._get_inactive_language_code()
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), lang_code),
+ )
+ self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+ next_url = reverse("channels")
+ lang_code = None
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code, next_url), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("channels"), "en"),
+ )
+ self.assertFalse(LANGUAGE_SESSION_KEY in self.client.session)
+
+ def test_setlang_null_next_invalid(self):
+ """
+ The set_language view can be used to change the session language.
+ The user is redirected to user redirect if the "next" argument is invalid.
+ """
+ lang_code = self._get_inactive_language_code()
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), lang_code),
+ )
+ self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
+ next_url = "/not/a/real/url"
+ lang_code = None
+ response = self.client.post(reverse("set_language"), self.set_post_data(lang_code, next_url), content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.content.decode("utf-8"),
+ translate_url(reverse("base"), "en"),
+ )
+ self.assertFalse(LANGUAGE_SESSION_KEY in self.client.session)
+
+ def test_setlang_get(self):
+ """
+ The set_language view is forbidden to be accessed via GET
+ """
+ lang_code = self._get_inactive_language_code()
+ response = self.client.get(reverse("set_language"), params=self.set_post_data(lang_code), content_type='application/json')
+ self.assertEqual(type(response), HttpResponseNotAllowed)
diff --git a/contentcuration/contentcuration/views/base.py b/contentcuration/contentcuration/views/base.py
index 3347513239..9a7339481c 100644
--- a/contentcuration/contentcuration/views/base.py
+++ b/contentcuration/contentcuration/views/base.py
@@ -1,7 +1,6 @@
import json
from builtins import str
from urllib.parse import urlsplit
-from urllib.parse import urlunsplit
from django.conf import settings
from django.contrib.auth.decorators import login_required
@@ -22,6 +21,8 @@
from django.urls import reverse
from django.urls import reverse_lazy
from django.urls import translate_url
+from django.utils.http import url_has_allowed_host_and_scheme
+from django.utils.translation import check_for_language
from django.utils.translation import get_language
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.views.decorators.http import require_POST
@@ -51,7 +52,6 @@
from contentcuration.models import License
from contentcuration.models import TaskResult
from contentcuration.serializers import SimplifiedChannelProbeCheckSerializer
-from contentcuration.utils.i18n import SUPPORTED_LANGUAGES
from contentcuration.utils.messages import get_messages
from contentcuration.viewsets.channelset import PublicChannelSetSerializer
@@ -342,53 +342,70 @@ def activate_channel_endpoint(request):
return HttpResponse(json.dumps({"success": True}))
-# Taken from kolibri.core.views which was
-# modified from django.views.i18n
@require_POST
+# flake8: noqa: C901
def set_language(request):
"""
+ We are using set_language from official Django set_language redirect view
+ https://docs.djangoproject.com/en/3.2/_modules/django/views/i18n/#set_language
+ and slighty modifying it according to our use case as we donot use AJAX, hence we donot
+ redirect rather just respond the required URL!
+
+
Since this view changes how the user will see the rest of the site, it must
only be accessed as a POST request. If called as a GET request, it will
error.
"""
payload = json.loads(request.body)
lang_code = payload.get(LANGUAGE_QUERY_PARAMETER)
- next_url = urlsplit(payload.get("next")) if payload.get("next") else None
- if lang_code and lang_code in SUPPORTED_LANGUAGES:
- if next_url and is_valid_path(next_url.path):
- # If it is a recognized path, then translate it to the new language and return it.
- next_path = urlunsplit(
- (
- next_url[0],
- next_url[1],
- translate_url(next_url[2], lang_code),
- next_url[3],
- next_url[4],
- )
- )
- else:
- # Just redirect to the base URL w/ the lang_code
- next_path = translate_url(reverse('base'), lang_code)
- response = HttpResponse(next_path)
- if hasattr(request, "session"):
- request.session[LANGUAGE_SESSION_KEY] = lang_code
- else:
- lang_code = get_language()
- if next_url and is_valid_path(next_url.path):
- # If it is a recognized path, then translate it using the default language code for this device
- next_path = urlunsplit(
- (
- next_url[0],
- next_url[1],
- translate_url(next_url[2], lang_code),
- next_url[3],
- next_url[4],
- )
+ next_url = payload.get("next")
+
+ if (
+ (next_url or request.accepts('text/html')) and
+ not url_has_allowed_host_and_scheme(
+ url=next_url,
+ allowed_hosts={request.get_host()},
+ require_https=request.is_secure(),
+ )
+ ):
+ next_url = request.META.get('HTTP_REFERER')
+ if not url_has_allowed_host_and_scheme(
+ url=next_url,
+ allowed_hosts={request.get_host()},
+ require_https=request.is_secure(),
+ ):
+ next_url = translate_url(reverse('base'), lang_code)
+ next_url_split = urlsplit(next_url) if next_url else None
+ if next_url and not is_valid_path(next_url_split.path):
+ next_url = translate_url(reverse('base'), lang_code)
+ response = HttpResponse(next_url) if next_url else HttpResponse(status=204)
+ if request.method == 'POST':
+ if lang_code and check_for_language(lang_code):
+ if next_url:
+ next_trans = translate_url(next_url, lang_code)
+ if next_trans != next_url:
+ response = HttpResponse(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,
+ max_age=settings.LANGUAGE_COOKIE_AGE,
+ path=settings.LANGUAGE_COOKIE_PATH,
+ domain=settings.LANGUAGE_COOKIE_DOMAIN,
+ secure=settings.LANGUAGE_COOKIE_SECURE,
+ httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
+ samesite=settings.LANGUAGE_COOKIE_SAMESITE,
)
else:
- # Just redirect to the base URL w/ the lang_code, likely the default language
- next_path = translate_url(reverse('base'), lang_code)
- response = HttpResponse(next_path)
- if hasattr(request, "session"):
- request.session.pop(LANGUAGE_SESSION_KEY, "")
+ lang_code = get_language()
+ if lang_code and check_for_language(lang_code):
+ if next_url:
+ next_trans = translate_url(next_url, lang_code)
+ if next_trans != next_url:
+ response = HttpResponse(next_trans)
+ if hasattr(request, "session"):
+ request.session.pop(LANGUAGE_SESSION_KEY, "")
+
return response