Commit 64291d3c by Asad Iqbal

Merge pull request #11391 from edx/asadiqbal08/WL-272

asadiqbal08/WL-272 - Studio Language Selection
parents ccbd88ac 64778cdf
......@@ -76,7 +76,8 @@
"PREVIEW_LMS_BASE": "localhost:8003",
"ALLOW_ALL_ADVANCED_COMPONENTS": true,
"ENABLE_CONTENT_LIBRARIES": true,
"ENABLE_SPECIAL_EXAMS": true
"ENABLE_SPECIAL_EXAMS": true,
"SHOW_LANGUAGE_SELECTOR": true
},
"FEEDBACK_SUBMISSION_EMAIL": "",
"GITHUB_REPO_ROOT": "** OVERRIDDEN **",
......
......@@ -190,6 +190,9 @@ FEATURES = {
'ENABLE_SPECIAL_EXAMS': False,
'ORGANIZATIONS_APP': False,
# Show Language selector
'SHOW_LANGUAGE_SELECTOR': False,
}
ENABLE_JASMINE = False
......@@ -328,6 +331,9 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
# This is used to set or update the user language preferences.
'lang_pref.middleware.LanguagePreferenceMiddleware',
# Allows us to dark-launch particular languages
'dark_lang.middleware.DarkLangMiddleware',
......@@ -1119,6 +1125,8 @@ OAUTH_OIDC_ISSUER = 'https://www.example.com/oauth2'
# 5 minute expiration time for JWT id tokens issued for external API requests.
OAUTH_ID_TOKEN_EXPIRATION = 5 * 60
USERNAME_PATTERN = r'(?P<username>[\w.@+-]+)'
# Partner support link for CMS footer
PARTNER_SUPPORT_EMAIL = ''
......
......@@ -87,6 +87,7 @@
"ova": 'js/vendor/ova/ova',
"catch": 'js/vendor/ova/catch/js/catch',
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
"lang_edx": "js/src/lang_edx",
// end of Annotation tool files
// externally hosted files
......@@ -196,6 +197,9 @@
"tinymce": {
exports: "tinymce"
},
"lang_edx": {
deps: ["jquery"]
},
"mathjax": {
exports: "MathJax",
init: function() {
......
define(['js/base', 'coffee/src/main', 'js/src/logger', 'datepair', 'accessibility',
'ieshim', 'tooltip_manager']);
'ieshim', 'tooltip_manager', 'lang_edx']);
......@@ -43,8 +43,19 @@
vertical-align: middle;
}
.user-language-selector {
width: 120px;
display: inline-block;
margin: 0 10px 0 5px;
vertical-align: sub;
.language-selector {
width: 120px;
}
}
.nav-account {
width: 100%;
width: auto;
}
// basic layout - nav items
......@@ -208,6 +219,13 @@
}
}
}
.settings-language-form {
margin-top: 4px;
.language-selector {
width: 130px;
}
}
}
// ====================
......@@ -229,11 +247,11 @@
.is-signedin {
.wrapper-l {
width: flex-grid(9,12);
width: flex-grid(8,12);
}
.wrapper-r {
width: flex-grid(3,12);
width: flex-grid(4,12);
}
.branding {
......@@ -253,11 +271,11 @@
.wrapper-header {
.wrapper-l {
width: flex-grid(9,12);
width: flex-grid(8,12);
}
.wrapper-r {
width: flex-grid(3,12);
width: flex-grid(4,12);
}
.branding {
......
......@@ -6,7 +6,6 @@
from contentstore.context_processors import doc_url
%>
<%page args="online_help_token"/>
<div class="wrapper-header wrapper" id="view-top">
<header class="primary" role="banner">
......@@ -188,6 +187,32 @@
</div>
<div class="wrapper wrapper-r">
% if static.show_language_selector():
<% languages = static.get_released_languages() %>
% if len(languages) > 1:
<nav class="user-language-selector">
<form action="/i18n/setlang/" method="post" class="settings-language-form" id="language-settings-form">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
% if user.is_authenticated():
<input title="preference api" type="hidden" id="preference-api-url" class="url-endpoint" value="${reverse('preferences_api', kwargs={'username': user.username})}" data-user-is-authenticated="true">
% else:
<input title="session update url" type="hidden" id="update-session-url" class="url-endpoint" value="${reverse('session_language')}" data-user-is-authenticated="false">
% endif
<label><span class="sr">${_("Choose Language")}</span>
<select class="input select language-selector" id="settings-language-value" name="language">
% for language in languages:
% if language[0] == LANGUAGE_CODE:
<option value="${language[0]}" selected="selected">${language[1]}</option>
% else:
<option value="${language[0]}" >${language[1]}</option>
% endif
% endfor
</select>
</label>
</form>
</nav>
% endif
% endif
% if user.is_authenticated():
<nav class="nav-account nav-is-signedin nav-dd ui-right" aria-label="${_('Account')}">
<h2 class="sr">${_("Account Navigation")}</h2>
......@@ -195,7 +220,6 @@
<li class="nav-item nav-account-help">
<h3 class="title"><span class="label"><a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_("Contextual Online Help")}" target="_blank">${_("Help")}</a></span></h3>
</li>
<li class="nav-item nav-account-user">
<h3 class="title"><span class="label"><span class="label-prefix sr">${_("Currently signed in as:")}</span><span class="account-username" title="${ user.username }">${ user.username }</span></span> <i class="icon fa fa-caret-down ui-toggle-dd"></i></h3>
......
......@@ -52,6 +52,14 @@ urlpatterns = patterns(
url(r'^heartbeat$', include('heartbeat.urls')),
url(r'^user_api/', include('openedx.core.djangoapps.user_api.legacy_urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),
# User API endpoints
url(r'^api/user/', include('openedx.core.djangoapps.user_api.urls')),
# Update session view
url(r'^lang_pref/session_language', 'lang_pref.views.update_session_language', name='session_language'),
)
# User creation and updating views
......
......@@ -2,7 +2,7 @@
Middleware for Language Preferences
"""
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, delete_user_preference
from lang_pref import LANGUAGE_KEY
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.utils.translation.trans_real import parse_accept_lang_header
......@@ -21,20 +21,26 @@ class LanguagePreferenceMiddleware(object):
"""
If a user's UserPreference contains a language preference, use the user's preference.
"""
languages = released_languages()
system_released_languages = [seq[0] for seq in languages]
# If the user is logged in, check for their language preference
if request.user.is_authenticated():
# Get the user's language preference
user_pref = get_user_preference(request.user, LANGUAGE_KEY)
# Set it to the LANGUAGE_SESSION_KEY (Django-specific session setting governing language pref)
if user_pref:
request.session[LANGUAGE_SESSION_KEY] = user_pref
else:
# Setting the session language to the browser language, if it is supported.
preferred_language = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
lang_headers = [seq[0] for seq in parse_accept_lang_header(preferred_language)]
languages = released_languages()
for browser_lang in lang_headers:
if browser_lang in [seq[0] for seq in languages]:
if request.session.get(LANGUAGE_SESSION_KEY, None) != browser_lang:
request.session[LANGUAGE_SESSION_KEY] = unicode(browser_lang)
break
if user_pref in system_released_languages:
request.session[LANGUAGE_SESSION_KEY] = user_pref
else:
delete_user_preference(request.user, LANGUAGE_KEY)
else:
preferred_language = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
lang_headers = [seq[0] for seq in parse_accept_lang_header(preferred_language)]
# Setting the session language to the browser language, if it is supported.
for browser_lang in lang_headers:
if browser_lang in system_released_languages:
if request.session.get(LANGUAGE_SESSION_KEY, None) is None:
request.session[LANGUAGE_SESSION_KEY] = unicode(browser_lang)
break
......@@ -7,6 +7,7 @@ from lang_pref.middleware import LanguagePreferenceMiddleware
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference, get_user_preference
from lang_pref import LANGUAGE_KEY
from student.tests.factories import UserFactory
from student.tests.factories import AnonymousUserFactory
import mock
......@@ -20,6 +21,7 @@ class TestUserPreferenceMiddleware(TestCase):
self.middleware = LanguagePreferenceMiddleware()
self.session_middleware = SessionMiddleware()
self.user = UserFactory.create()
self.anonymous_user = AnonymousUserFactory()
self.request = RequestFactory().get('/somewhere')
self.request.user = self.user
self.request.META['HTTP_ACCEPT_LANGUAGE'] = 'ar;q=1.0' # pylint: disable=no-member
......@@ -30,12 +32,16 @@ class TestUserPreferenceMiddleware(TestCase):
self.middleware.process_request(self.request)
self.assertNotIn(LANGUAGE_SESSION_KEY, self.request.session)
@mock.patch('lang_pref.middleware.released_languages', mock.Mock(return_value=[('eo', 'esperanto')]))
def test_language_in_user_prefs(self):
# language set in the user preferences and not the session
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
self.middleware.process_request(self.request)
self.assertEquals(self.request.session[LANGUAGE_SESSION_KEY], 'eo')
@mock.patch('lang_pref.middleware.released_languages', mock.Mock(
return_value=[('en', 'english'), ('eo', 'esperanto')]
))
def test_language_in_session(self):
# language set in both the user preferences and session,
# preference should get precedence. The session will hold the last value,
......@@ -53,16 +59,29 @@ class TestUserPreferenceMiddleware(TestCase):
mock.Mock(return_value=[('eo', 'dummy Esperanto'), ('ar', 'arabic')]))
def test_supported_browser_language_in_session(self):
"""
test: browser language should be set in user preferences if it is supported by system.
test: browser language should be set in user session if it is supported by system for unauthenticated user.
"""
self.assertEquals(get_user_preference(self.request.user, LANGUAGE_KEY), None)
self.request.user = self.anonymous_user
self.middleware.process_request(self.request)
self.assertEqual(self.request.session[LANGUAGE_SESSION_KEY], 'ar') # pylint: disable=no-member
@mock.patch('lang_pref.middleware.released_languages', mock.Mock(return_value=[('en', 'english')]))
def test_browser_language_not_be_in_session(self):
"""
test: browser language should not be set in user preferences if it is not supported by system.
test: browser language should not be set in user session if it is not supported by system.
"""
self.request.user = self.anonymous_user
self.middleware.process_request(self.request)
self.assertNotEqual(self.request.session.get(LANGUAGE_SESSION_KEY), 'ar') # pylint: disable=no-member
@mock.patch('lang_pref.middleware.released_languages', mock.Mock(
return_value=[('en', 'english'), ('ar', 'arabic')]
))
def test_delete_user_lang_preference_not_supported_by_system(self):
"""
test: user preferred language has been removed from user preferences model if it is not supported by system
for authenticated users.
"""
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
self.middleware.process_request(self.request)
self.assertEqual(get_user_preference(self.request.user, LANGUAGE_KEY), None)
"""
Tests: lang pref views
"""
import json
from django.test import TestCase
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
from student.tests.factories import UserFactory
from django.utils.translation import LANGUAGE_SESSION_KEY
from django.contrib.sessions.middleware import SessionMiddleware
from django.utils.translation import get_language
class TestLangPrefView(TestCase):
"""
Language preference view tests.
"""
def setUp(self):
super(TestLangPrefView, self).setUp()
self.session_middleware = SessionMiddleware()
self.user = UserFactory.create()
self.request = RequestFactory().get('/somewhere')
self.request.user = self.user
self.session_middleware.process_request(self.request)
def test_language_session_update(self):
# test language session updating correctly.
self.request.session[LANGUAGE_SESSION_KEY] = 'ar' # pylint: disable=no-member
response = self.client.patch(reverse("session_language"), json.dumps({'pref-lang': 'eo'}))
self.assertEqual(response.status_code, 200)
self.client.get('/')
self.assertEquals(get_language(), 'eo')
response = self.client.patch(reverse("session_language"), json.dumps({'pref-lang': 'en'}))
self.assertEqual(response.status_code, 200)
self.client.get('/')
self.assertEquals(get_language(), 'en')
"""
Language Preference Views
"""
import json
from django.conf import settings
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils.translation import LANGUAGE_SESSION_KEY
from lang_pref import LANGUAGE_KEY
from django.http import HttpResponse
@ensure_csrf_cookie
def update_session_language(request):
"""
Update the language session key.
"""
if request.method == 'PATCH':
data = json.loads(request.body)
language = data.get(LANGUAGE_KEY, settings.LANGUAGE_CODE)
if request.session.get(LANGUAGE_SESSION_KEY, None) != language:
request.session[LANGUAGE_SESSION_KEY] = unicode(language)
return HttpResponse(200)
......@@ -6,6 +6,7 @@ from mako.exceptions import TemplateLookupException
from openedx.core.djangoapps.theming.helpers import get_page_title_breadcrumbs, get_value, get_template_path, get_themed_template_path, is_request_in_themed_site
from certificates.api import get_asset_url_by_slug
from lang_pref.api import released_languages
%>
<%def name='url(file, raw=False)'><%
......@@ -132,3 +133,7 @@ else:
<%def name="show_language_selector()"><%
return settings.FEATURES.get('SHOW_LANGUAGE_SELECTOR', False)
%></%def>
<%def name="get_released_languages()"><%
return released_languages()
%></%def>
<form action="/i18n/setlang/" method="post" class="settings-language-form" id="language-settings-form">
<input title="preference api" type="hidden" id="preference-api-url" value="/api/user/v1/preferences/test1/">
<input title="preference api" class="url-endpoint" type="hidden" id="preference-api-url" value="/api/user/v1/preferences/test1/" data-user-is-authenticated="true">
<label><span class="sr">Choose Language</span>
<select class="input select" id="settings-language-value" name="language">
<option value="en" selected="selected">English</option>
......
var edx = edx || {},
Language = (function() {
var Language = (function() {
'use strict';
var preference_api_url,
settings_language_selector,
var settings_language_selector,
self = null;
return {
init: function() {
preference_api_url = $('#preference-api-url').val();
settings_language_selector = $('#settings-language-value');
self = this;
this.listenForLanguagePreferenceChange();
......@@ -18,32 +15,49 @@ var edx = edx || {},
*/
listenForLanguagePreferenceChange: function() {
settings_language_selector.change(function(event) {
var language = this.value;
var language = this.value,
url = $('.url-endpoint').val(),
is_user_authenticated = JSON.parse($('.url-endpoint').data('user-is-authenticated'));
event.preventDefault();
$.ajax({
type: 'PATCH',
data: JSON.stringify({'pref-lang': language}) ,
url: preference_api_url,
dataType: 'json',
contentType: "application/merge-patch+json",
beforeSend: function (xhr) {
xhr.setRequestHeader("X-CSRFToken", $('#csrf_token').val());
self.submitAjaxRequest(language, url, function() {
if (is_user_authenticated) {
// User language preference has been set successfully
// Now submit the form in success callback.
$('#language-settings-form').submit();
} else {
self.refresh();
}
}).done(function () {
// User language preference has been set successfully
// Now submit the form in success callback.
$("#language-settings-form").submit();
}).fail(function() {
self.refresh();
});
});
},
/**
* Send an ajax request to set user language preferences.
*/
submitAjaxRequest: function(language, url, callback) {
$.ajax({
type: 'PATCH',
data: JSON.stringify({'pref-lang': language}) ,
url: url,
dataType: 'json',
contentType: 'application/merge-patch+json',
notifyOnError: false,
beforeSend: function (xhr) {
xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken'));
}
}).done(function () {
callback();
}).fail(function() {
self.refresh();
});
},
/**
* refresh the page.
*/
refresh: function () {
// reloading the page so we can get the latest state of realsesd languages from model
// reloading the page so we can get the latest state of released languages from model
location.reload();
}
......
......@@ -225,6 +225,17 @@ class DashboardPage(PageObject):
return True
return False
@property
def language_selector(self):
"""
return language selector
"""
self.wait_for_element_visibility(
'#settings-language-value',
'Language selector element is available'
)
return self.q(css='#settings-language-value')
class DashboardPageWithPrograms(DashboardPage):
"""
......
......@@ -10,6 +10,11 @@ from ...fixtures.programs import ProgramsFixture
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.library import LibraryEditPage
from ...pages.studio.index import DashboardPage, DashboardPageWithPrograms
from ...pages.lms.account_settings import AccountSettingsPage
from ..helpers import (
select_option_by_text,
get_selected_option_text
)
class CreateLibraryTest(WebAppTest):
......@@ -139,3 +144,37 @@ class DashboardProgramsTabTest(WebAppTest):
self.dashboard_page.visit()
self.assertFalse(self.dashboard_page.is_programs_tab_present())
self.assertFalse(self.dashboard_page.is_new_program_button_present())
class StudioLanguageTest(WebAppTest):
""" Test suite for the Studio Language """
def setUp(self):
super(StudioLanguageTest, self).setUp()
self.dashboard_page = DashboardPage(self.browser)
self.account_settings = AccountSettingsPage(self.browser)
AutoAuthPage(self.browser).visit()
def test_studio_language_change(self):
"""
Scenario: Ensure that language selection is working fine.
First I go to the user dashboard page in studio. I can see 'English' is selected by default.
Then I choose 'Dummy Language' from drop down (at top of the page).
Then I visit the student account settings page and I can see the language has been updated to 'Dummy Language'
in both drop downs.
"""
dummy_language = u'Dummy Language (Esperanto)'
self.dashboard_page.visit()
language_selector = self.dashboard_page.language_selector
self.assertEqual(
get_selected_option_text(language_selector),
u'English'
)
select_option_by_text(language_selector, dummy_language)
self.dashboard_page.wait_for_ajax()
self.account_settings.visit()
self.assertEqual(self.account_settings.value_for_dropdown_field('pref-lang'), dummy_language)
self.assertEqual(
get_selected_option_text(language_selector),
u'Dummy Language (Esperanto)'
)
......@@ -365,7 +365,7 @@ FEATURES = {
# Enable LTI Provider feature.
'ENABLE_LTI_PROVIDER': False,
# Show LMS Language selector
# Show Language selector.
'SHOW_LANGUAGE_SELECTOR': False,
}
......
......@@ -119,7 +119,10 @@ header.global {
margin: 0;
.settings-language-form {
margin-top: 4px;
margin: 4px;
.language-selector {
width: 120px;
}
}
> .primary {
......
......@@ -14,7 +14,6 @@ from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_
from branding import api as branding_api
# app that handles site status messages
from status.status import get_site_status_msg
from lang_pref.api import released_languages
%>
## Provide a hook for themes to inject branding on top.
......@@ -38,7 +37,6 @@ site_status_msg = get_site_status_msg(course_id)
</div>
% endif
</%block>
<header id="global-navigation" class="global ${"slim" if course else ""}" >
<nav class="nav-wrapper" aria-label="${_('Global')}">
<h1 class="logo">
......@@ -79,28 +77,6 @@ site_status_msg = get_site_status_msg(course_id)
</%block>
</ol>
<ol class="user">
% if static.show_language_selector():
<% languages = released_languages() %>
% if len(languages) > 1:
<li class="primary">
<form action="/i18n/setlang/" method="post" class="settings-language-form" id="language-settings-form">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
<input title="preference api" type="hidden" id="preference-api-url" value="${reverse('preferences_api', kwargs={'username': user.username})}">
<label><span class="sr">${_("Choose Language")}</span>
<select class="input select" id="settings-language-value" name="language">
% for language in languages:
% if language[0] == LANGUAGE_CODE:
<option value="${language[0]}" selected="selected">${language[1]}</option>
% else:
<option value="${language[0]}" >${language[1]}</option>
% endif
% endfor
</select>
</label>
</form>
</li>
% endif
% endif
<li class="primary">
<a href="${reverse('dashboard')}" class="user-link">
<span class="sr">${_("Dashboard for:")}</span>
......@@ -186,7 +162,35 @@ site_status_msg = get_site_status_msg(course_id)
</%block>
</ol>
% endif
</nav>
% if static.show_language_selector():
<% languages = static.get_released_languages() %>
% if len(languages) > 1:
<ol class="user">
<li class="primary">
<form action="/i18n/setlang/" method="post" class="settings-language-form" id="language-settings-form">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
% if user.is_authenticated():
<input title="preference api" type="hidden" class="url-endpoint" value="${reverse('preferences_api', kwargs={'username': user.username})}" data-user-is-authenticated="true">
% else:
<input title="session update url" type="hidden" class="url-endpoint" value="${reverse('session_language')}" data-user-is-authenticated="false">
% endif
<label><span class="sr">${_("Choose Language")}</span>
<select class="input select language-selector" id="settings-language-value" name="language">
% for language in languages:
% if language[0] == LANGUAGE_CODE:
<option value="${language[0]}" selected="selected">${language[1]}</option>
% else:
<option value="${language[0]}" >${language[1]}</option>
% endif
% endfor
</select>
</label>
</form>
</li>
</ol>
% endif
% endif
</nav>
</header>
% if course:
<!--[if lte IE 9]>
......
......@@ -103,6 +103,9 @@ urlpatterns = (
url(r'^rss_proxy/', include('rss_proxy.urls', namespace='rss_proxy')),
url(r'^api/organizations/', include('organizations.urls', namespace='organizations')),
# Update session view
url(r'^lang_pref/session_language', 'lang_pref.views.update_session_language', name='session_language'),
# Multiple course modes and identity verification
# TODO Namespace these!
url(r'^course_modes/', include('course_modes.urls')),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment