Merge pull request #2419 from edx/talbs/lms-languagemenu
LMS: Adds Language Selection Menu
This commit is contained in:
@@ -456,8 +456,13 @@ INSTALLED_APPS = (
|
||||
|
||||
# Dark-launching languages
|
||||
'dark_lang',
|
||||
|
||||
# Student identity reverification
|
||||
'reverification',
|
||||
|
||||
# User preferences
|
||||
'user_api',
|
||||
'django_openid_auth',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ urlpatterns = patterns('', # nopep8
|
||||
|
||||
url(r'^xmodule/', include('pipeline_js.urls')),
|
||||
url(r'^heartbeat$', include('heartbeat.urls')),
|
||||
|
||||
url(r'^user_api/', include('user_api.urls')),
|
||||
url(r'^lang_pref/', include('lang_pref.urls')),
|
||||
)
|
||||
|
||||
# User creation and updating views
|
||||
|
||||
@@ -19,6 +19,8 @@ class DarkLangConfig(ConfigurationModel):
|
||||
def released_languages_list(self):
|
||||
"""
|
||||
``released_languages`` as a list of language codes.
|
||||
|
||||
Example: ['it', 'de-at', 'es', 'pt-br']
|
||||
"""
|
||||
if not self.released_languages.strip(): # pylint: disable=no-member
|
||||
return []
|
||||
|
||||
6
common/djangoapps/lang_pref/__init__.py
Normal file
6
common/djangoapps/lang_pref/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Useful information for setting the language preference
|
||||
"""
|
||||
|
||||
# this is the UserPreference key for the user's preferred language
|
||||
LANGUAGE_KEY = 'pref-lang'
|
||||
25
common/djangoapps/lang_pref/middleware.py
Normal file
25
common/djangoapps/lang_pref/middleware.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Middleware for Language Preferences
|
||||
"""
|
||||
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
class LanguagePreferenceMiddleware(object):
|
||||
"""
|
||||
Middleware for user preferences.
|
||||
|
||||
Ensures that, once set, a user's preferences are reflected in the page
|
||||
whenever they are logged in.
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
If a user's UserPreference contains a language preference and there is
|
||||
no language set on the session (i.e. from dark language overrides), use the user's preference.
|
||||
"""
|
||||
if request.user.is_authenticated() and 'django_language' not in request.session:
|
||||
user_pref = UserPreference.get_preference(request.user, LANGUAGE_KEY)
|
||||
if user_pref:
|
||||
request.session['django_language'] = user_pref
|
||||
42
common/djangoapps/lang_pref/tests/test_middleware.py
Normal file
42
common/djangoapps/lang_pref/tests/test_middleware.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
|
||||
from lang_pref.middleware import LanguagePreferenceMiddleware
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class TestUserPreferenceMiddleware(TestCase):
|
||||
"""
|
||||
Tests to make sure user preferences are getting properly set in the middleware
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = LanguagePreferenceMiddleware()
|
||||
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_no_language_set_in_session_or_prefs(self):
|
||||
# nothing set in the session or the prefs
|
||||
self.middleware.process_request(self.request)
|
||||
self.assertNotIn('django_language', self.request.session)
|
||||
|
||||
def test_language_in_user_prefs(self):
|
||||
# language set in the user preferences and not the session
|
||||
UserPreference.set_preference(self.user, LANGUAGE_KEY, 'eo')
|
||||
self.middleware.process_request(self.request)
|
||||
self.assertEquals(self.request.session['django_language'], 'eo')
|
||||
|
||||
def test_language_in_session(self):
|
||||
# language set in both the user preferences and session,
|
||||
# session should get precedence
|
||||
self.request.session['django_language'] = 'en'
|
||||
UserPreference.set_preference(self.user, LANGUAGE_KEY, 'eo')
|
||||
self.middleware.process_request(self.request)
|
||||
|
||||
self.assertEquals(self.request.session['django_language'], 'en')
|
||||
34
common/djangoapps/lang_pref/tests/test_views.py
Normal file
34
common/djangoapps/lang_pref/tests/test_views.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Tests for the language setting view
|
||||
"""
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
class TestLanguageSetting(TestCase):
|
||||
"""
|
||||
Test setting languages
|
||||
"""
|
||||
def test_set_preference_happy(self):
|
||||
user = UserFactory.create()
|
||||
self.client.login(username=user.username, password='test')
|
||||
|
||||
lang = 'en'
|
||||
response = self.client.post(reverse('lang_pref_set_language'), {'language': lang})
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
user_pref = UserPreference.get_preference(user, LANGUAGE_KEY)
|
||||
self.assertEqual(user_pref, lang)
|
||||
|
||||
def test_set_preference_missing_lang(self):
|
||||
user = UserFactory.create()
|
||||
self.client.login(username=user.username, password='test')
|
||||
|
||||
response = self.client.post(reverse('lang_pref_set_language'))
|
||||
|
||||
self.assertEquals(response.status_code, 400)
|
||||
|
||||
self.assertIsNone(UserPreference.get_preference(user, LANGUAGE_KEY))
|
||||
10
common/djangoapps/lang_pref/urls.py
Normal file
10
common/djangoapps/lang_pref/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Urls for managing language preferences
|
||||
"""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^setlang/', 'lang_pref.views.set_language', name='lang_pref_set_language')
|
||||
)
|
||||
23
common/djangoapps/lang_pref/views.py
Normal file
23
common/djangoapps/lang_pref/views.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Views for accessing language preferences
|
||||
"""
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
@login_required
|
||||
def set_language(request):
|
||||
"""
|
||||
This view is called when the user would like to set a language preference
|
||||
"""
|
||||
user = request.user
|
||||
lang_pref = request.POST.get('language', None)
|
||||
|
||||
if lang_pref:
|
||||
UserPreference.set_preference(user, LANGUAGE_KEY, lang_pref)
|
||||
return HttpResponse('{"success": true}')
|
||||
|
||||
return HttpResponseBadRequest('no language provided')
|
||||
@@ -45,6 +45,7 @@ from student.firebase_token_generator import create_token
|
||||
|
||||
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
from dark_lang.models import DarkLangConfig
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -61,6 +62,8 @@ import external_auth.views
|
||||
|
||||
from bulk_email.models import Optout, CourseAuthorization
|
||||
import shoppingcart
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
import track.views
|
||||
|
||||
@@ -468,23 +471,42 @@ def dashboard(request):
|
||||
# we'll display the banner
|
||||
denied_banner = any(item.display for item in reverifications["denied"])
|
||||
|
||||
context = {'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'external_auth_map': external_auth_map,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_modes,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
'verification_status': verification_status,
|
||||
'verification_msg': verification_msg,
|
||||
'show_refund_option_for': show_refund_option_for,
|
||||
'denied_banner': denied_banner,
|
||||
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
|
||||
}
|
||||
language_options = DarkLangConfig.current().released_languages_list
|
||||
|
||||
# add in the default language if it's not in the list of released languages
|
||||
if settings.LANGUAGE_CODE not in language_options:
|
||||
language_options.append(settings.LANGUAGE_CODE)
|
||||
|
||||
# try to get the prefered language for the user
|
||||
cur_lang_code = UserPreference.get_preference(request.user, LANGUAGE_KEY)
|
||||
if cur_lang_code:
|
||||
# if the user has a preference, get the name from the code
|
||||
current_language = settings.LANGUAGE_DICT[cur_lang_code]
|
||||
else:
|
||||
# if the user doesn't have a preference, use the default language
|
||||
current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
|
||||
|
||||
context = {
|
||||
'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'external_auth_map': external_auth_map,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_modes,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
'verification_status': verification_status,
|
||||
'verification_msg': verification_msg,
|
||||
'show_refund_option_for': show_refund_option_for,
|
||||
'denied_banner': denied_banner,
|
||||
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
|
||||
'language_options': language_options,
|
||||
'current_language': current_language,
|
||||
'current_language_code': cur_lang_code,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
35
common/djangoapps/user_api/models.py
Normal file
35
common/djangoapps/user_api/models.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
class UserPreference(models.Model):
|
||||
"""A user's preference, stored as generic text to be processed by client"""
|
||||
user = models.ForeignKey(User, db_index=True, related_name="+")
|
||||
key = models.CharField(max_length=255, db_index=True)
|
||||
value = models.TextField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "key")
|
||||
|
||||
@classmethod
|
||||
def set_preference(cls, user, preference_key, preference_value):
|
||||
"""
|
||||
Sets the user preference for a given key
|
||||
"""
|
||||
user_pref, _ = cls.objects.get_or_create(user=user, key=preference_key)
|
||||
user_pref.value = preference_value
|
||||
user_pref.save()
|
||||
|
||||
@classmethod
|
||||
def get_preference(cls, user, preference_key, default=None):
|
||||
"""
|
||||
Gets the user preference value for a given key
|
||||
|
||||
Returns the given default if there isn't a preference for the given key
|
||||
"""
|
||||
|
||||
try:
|
||||
user_pref = cls.objects.get(user=user, key=preference_key)
|
||||
return user_pref.value
|
||||
except cls.DoesNotExist:
|
||||
return default
|
||||
@@ -2,6 +2,7 @@ from django.db import IntegrityError
|
||||
from django.test import TestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from user_api.tests.factories import UserPreferenceFactory
|
||||
from user_api.models import UserPreference
|
||||
|
||||
|
||||
class UserPreferenceModelTest(TestCase):
|
||||
@@ -26,3 +27,21 @@ class UserPreferenceModelTest(TestCase):
|
||||
key="testkey3",
|
||||
value="\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\xad\xe5\x9b\xbd\xe6\x96\x87\xe5\xad\x97'"
|
||||
)
|
||||
|
||||
def test_get_set_preference(self):
|
||||
# Checks that you can set a preference and get that preference later
|
||||
# Also, tests that no preference is returned for keys that are not set
|
||||
|
||||
user = UserFactory.create()
|
||||
key = 'testkey'
|
||||
value = 'testvalue'
|
||||
|
||||
# does a round trip
|
||||
UserPreference.set_preference(user, key, value)
|
||||
pref = UserPreference.get_preference(user, key)
|
||||
|
||||
self.assertEqual(pref, value)
|
||||
|
||||
# get preference for key that doesn't exist for user
|
||||
pref = UserPreference.get_preference(user, 'testkey_none')
|
||||
self.assertIsNone(pref)
|
||||
@@ -1,6 +1,5 @@
|
||||
import base64
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
import json
|
||||
@@ -17,21 +16,9 @@ USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/"
|
||||
|
||||
|
||||
@override_settings(EDX_API_KEY=TEST_API_KEY)
|
||||
class UserApiTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super(UserApiTestCase, self).setUp()
|
||||
self.users = [
|
||||
UserFactory.create(
|
||||
email="test{0}@test.org".format(i),
|
||||
profile__name="Test {0}".format(i)
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
self.prefs = [
|
||||
UserPreferenceFactory.create(user=self.users[0], key="key0"),
|
||||
UserPreferenceFactory.create(user=self.users[0], key="key1"),
|
||||
UserPreferenceFactory.create(user=self.users[1], key="key0")
|
||||
]
|
||||
class ApiTestCase(TestCase):
|
||||
|
||||
LIST_URI = USER_LIST_URI
|
||||
|
||||
def basic_auth(self, username, password):
|
||||
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
|
||||
@@ -95,11 +82,41 @@ class UserApiTestCase(TestCase):
|
||||
"""Assert that the given response has the status code 403"""
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def assertHttpBadRequest(self, response):
|
||||
"""Assert that the given response has the status code 400"""
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def assertHttpMethodNotAllowed(self, response):
|
||||
"""Assert that the given response has the status code 405"""
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
|
||||
class EmptyUserTestCase(ApiTestCase):
|
||||
def test_get_list_empty(self):
|
||||
result = self.get_json(self.LIST_URI)
|
||||
self.assertEqual(result["count"], 0)
|
||||
self.assertIsNone(result["next"])
|
||||
self.assertIsNone(result["previous"])
|
||||
self.assertEqual(result["results"], [])
|
||||
|
||||
|
||||
class UserApiTestCase(ApiTestCase):
|
||||
def setUp(self):
|
||||
super(UserApiTestCase, self).setUp()
|
||||
self.users = [
|
||||
UserFactory.create(
|
||||
email="test{0}@test.org".format(i),
|
||||
profile__name="Test {0}".format(i)
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
self.prefs = [
|
||||
UserPreferenceFactory.create(user=self.users[0], key="key0"),
|
||||
UserPreferenceFactory.create(user=self.users[0], key="key1"),
|
||||
UserPreferenceFactory.create(user=self.users[1], key="key0")
|
||||
]
|
||||
|
||||
|
||||
class UserViewSetTest(UserApiTestCase):
|
||||
LIST_URI = USER_LIST_URI
|
||||
|
||||
@@ -137,17 +154,10 @@ class UserViewSetTest(UserApiTestCase):
|
||||
def test_basic_auth(self):
|
||||
# ensure that having basic auth headers in the mix does not break anything
|
||||
self.assertHttpOK(
|
||||
self.request_with_auth("get", self.LIST_URI, **self.basic_auth('someuser', 'somepass')))
|
||||
self.request_with_auth("get", self.LIST_URI,
|
||||
**self.basic_auth('someuser', 'somepass')))
|
||||
self.assertHttpForbidden(
|
||||
self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass')))
|
||||
|
||||
def test_get_list_empty(self):
|
||||
User.objects.all().delete()
|
||||
result = self.get_json(self.LIST_URI)
|
||||
self.assertEqual(result["count"], 0)
|
||||
self.assertIsNone(result["next"])
|
||||
self.assertIsNone(result["previous"])
|
||||
self.assertEqual(result["results"], [])
|
||||
self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass')))
|
||||
|
||||
def test_get_list_nonempty(self):
|
||||
result = self.get_json(self.LIST_URI)
|
||||
@@ -245,14 +255,6 @@ class UserPreferenceViewSetTest(UserApiTestCase):
|
||||
def test_debug_auth(self):
|
||||
self.assertHttpOK(self.client.get(self.LIST_URI))
|
||||
|
||||
def test_get_list_empty(self):
|
||||
UserPreference.objects.all().delete()
|
||||
result = self.get_json(self.LIST_URI)
|
||||
self.assertEqual(result["count"], 0)
|
||||
self.assertIsNone(result["next"])
|
||||
self.assertIsNone(result["previous"])
|
||||
self.assertEqual(result["results"], [])
|
||||
|
||||
def test_get_list_nonempty(self):
|
||||
result = self.get_json(self.LIST_URI)
|
||||
self.assertEqual(result["count"], 3)
|
||||
@@ -4,8 +4,8 @@ from rest_framework import authentication
|
||||
from rest_framework import filters
|
||||
from rest_framework import permissions
|
||||
from rest_framework import viewsets
|
||||
from user_api.models import UserPreference
|
||||
from user_api.serializers import UserSerializer, UserPreferenceSerializer
|
||||
from user_api.models import UserPreference
|
||||
|
||||
|
||||
class ApiKeyHeaderPermission(permissions.BasePermission):
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Student dashboard page.
|
||||
"""
|
||||
@@ -17,6 +18,14 @@ class DashboardPage(PageObject):
|
||||
def is_browser_on_page(self):
|
||||
return self.is_css_present('section.my-courses')
|
||||
|
||||
@property
|
||||
def current_courses_text(self):
|
||||
text_items = self.css_text('section#my-courses')
|
||||
if len(text_items) > 0:
|
||||
return text_items[0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def available_courses(self):
|
||||
"""
|
||||
@@ -59,3 +68,11 @@ class DashboardPage(PageObject):
|
||||
return "a.enter-course:nth-of-type({0})".format(link_index + 1)
|
||||
else:
|
||||
return None
|
||||
|
||||
def change_language(self, code):
|
||||
"""
|
||||
Change the language on the dashboard to the language corresponding with `code`.
|
||||
"""
|
||||
self.css_click(".edit-language")
|
||||
self.select_option("language", code)
|
||||
self.css_click("#submit-lang")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
E2E tests for the LMS.
|
||||
"""
|
||||
@@ -5,7 +6,7 @@ E2E tests for the LMS.
|
||||
from unittest import skip
|
||||
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
from bok_choy.promise import EmptyPromise, fulfill_before
|
||||
from bok_choy.promise import EmptyPromise, fulfill_before, fulfill, Promise
|
||||
|
||||
from .helpers import UniqueCourseTest, load_data_str
|
||||
from ..pages.studio.auto_auth import AutoAuthPage
|
||||
@@ -17,6 +18,7 @@ from ..pages.lms.course_info import CourseInfoPage
|
||||
from ..pages.lms.tab_nav import TabNavPage
|
||||
from ..pages.lms.course_nav import CourseNavPage
|
||||
from ..pages.lms.progress import ProgressPage
|
||||
from ..pages.lms.dashboard import DashboardPage
|
||||
from ..pages.lms.video import VideoPage
|
||||
from ..pages.xblock.acid import AcidView
|
||||
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
|
||||
@@ -68,6 +70,66 @@ class RegistrationTest(UniqueCourseTest):
|
||||
self.assertIn(self.course_info['display_name'], course_names)
|
||||
|
||||
|
||||
class LanguageTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that the change language functionality on the dashboard works
|
||||
"""
|
||||
|
||||
@property
|
||||
def _changed_lang_promise(self):
|
||||
def _check_func():
|
||||
text = self.dashboard_page.current_courses_text
|
||||
return (len(text) > 0, text)
|
||||
return Promise(_check_func, "language changed")
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initiailize dashboard page
|
||||
"""
|
||||
super(LanguageTest, self).setUp()
|
||||
self.dashboard_page = DashboardPage(self.browser)
|
||||
|
||||
self.test_new_lang = 'eo'
|
||||
# This string is unicode for "ÇÜRRÉNT ÇØÜRSÉS", which should appear in our Dummy Esperanto page
|
||||
# We store the string this way because Selenium seems to try and read in strings from
|
||||
# the HTML in this format. Ideally we could just store the raw ÇÜRRÉNT ÇØÜRSÉS string here
|
||||
self.current_courses_text = u'\xc7\xdcRR\xc9NT \xc7\xd6\xdcRS\xc9S'
|
||||
|
||||
self.username = "test"
|
||||
self.password = "testpass"
|
||||
self.email = "test@example.com"
|
||||
|
||||
def test_change_lang(self):
|
||||
AutoAuthPage(self.browser, course_id=self.course_id).visit()
|
||||
self.dashboard_page.visit()
|
||||
# Change language to Dummy Esperanto
|
||||
self.dashboard_page.change_language(self.test_new_lang)
|
||||
|
||||
changed_text = fulfill(self._changed_lang_promise)
|
||||
# We should see the dummy-language text on the page
|
||||
self.assertIn(self.current_courses_text, changed_text)
|
||||
|
||||
def test_language_persists(self):
|
||||
auto_auth_page = AutoAuthPage(self.browser, username=self.username, password=self.password, email=self.email, course_id=self.course_id)
|
||||
auto_auth_page.visit()
|
||||
|
||||
self.dashboard_page.visit()
|
||||
# Change language to Dummy Esperanto
|
||||
self.dashboard_page.change_language(self.test_new_lang)
|
||||
|
||||
# destroy session
|
||||
self.browser._cookie_manager.delete()
|
||||
|
||||
# log back in
|
||||
auto_auth_page.visit()
|
||||
|
||||
self.dashboard_page.visit()
|
||||
|
||||
changed_text = fulfill(self._changed_lang_promise)
|
||||
# We should see the dummy-language text on the page
|
||||
self.assertIn(self.current_courses_text, changed_text)
|
||||
|
||||
|
||||
class HighLevelTabTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that verify each of the high-level tabs available within a course.
|
||||
|
||||
1
common/test/db_fixtures/released_lang.json
Normal file
1
common/test/db_fixtures/released_lang.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"pk": 1, "model": "dark_lang.darklangconfig", "fields": {"change_date": "2100-01-30T20:34:20Z", "changed_by": null, "enabled": true, "released_languages": "en,eo"}}]
|
||||
@@ -1,12 +0,0 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
|
||||
|
||||
class UserPreference(models.Model):
|
||||
"""A user's preference, stored as generic text to be processed by client"""
|
||||
user = models.ForeignKey(User, db_index=True, related_name="+")
|
||||
key = models.CharField(max_length=255, db_index=True)
|
||||
value = models.TextField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "key")
|
||||
@@ -501,7 +501,8 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
|
||||
# Sourced from http://www.localeplanet.com/icu/ and wikipedia
|
||||
LANGUAGES = (
|
||||
('eo', u'Dummy Language (Esperanto)'), # Dummy language used for testing
|
||||
('en', u'English'),
|
||||
('eo', u'Dummy Language (Esperanto)'), # Dummy languaged used for testing
|
||||
('fake2', u'Fake translations'), # Another dummy language for testing (not pushed to prod)
|
||||
|
||||
('ach', u'Acholi'), # Acoli
|
||||
@@ -554,6 +555,8 @@ LANGUAGES = (
|
||||
('zh-tw', u'台灣正體'), # Chinese (Taiwan)
|
||||
)
|
||||
|
||||
LANGUAGE_DICT = dict(LANGUAGES)
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
@@ -698,6 +701,10 @@ MIDDLEWARE_CLASSES = (
|
||||
# Allows us to dark-launch particular languages
|
||||
'dark_lang.middleware.DarkLangMiddleware',
|
||||
|
||||
# Allows us to set user preferences
|
||||
# should be after DarkLangMiddleware
|
||||
'lang_pref.middleware.LanguagePreferenceMiddleware',
|
||||
|
||||
# Detects user-requested locale from 'accept-language' header in http request
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
|
||||
|
||||
@@ -97,6 +97,26 @@
|
||||
%ui-depth4 { z-index: 10000; }
|
||||
%ui-depth5 { z-index: 100000; }
|
||||
|
||||
// extends - UI - utility - nth-type style clearing
|
||||
%wipe-first-child {
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// extends - UI - utility - nth-type style clearing
|
||||
%wipe-last-child {
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// extends -hidden elems - screenreaders
|
||||
%text-sr {
|
||||
border: 0;
|
||||
|
||||
@@ -231,7 +231,8 @@
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
box-shadow: none;
|
||||
:hover, :focus {
|
||||
|
||||
&:hover, &:focus {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,6 +854,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
// status - language
|
||||
.status-language {
|
||||
|
||||
.icon {
|
||||
@include font-size(17);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.title .icon {
|
||||
opacity: 0.75; // needed to overcome bad specificity elsewhere
|
||||
}
|
||||
}
|
||||
|
||||
// status - verification
|
||||
.status-verification {
|
||||
|
||||
|
||||
@@ -325,3 +325,37 @@
|
||||
@extend .modal;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
|
||||
// CASE: language settings
|
||||
.modal-settings-language {
|
||||
|
||||
// general reset
|
||||
.list-input, .list-actions {
|
||||
@extend %ui-no-list;
|
||||
}
|
||||
|
||||
.settings-language-select .select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-input {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.actions-supplemental {
|
||||
padding: 0 ($baseline*2) $baseline ($baseline*2);
|
||||
|
||||
.list-actions-item {
|
||||
@extend %t-copy-sub1;
|
||||
color: $base-font-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from django.template import RequestContext %>
|
||||
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -82,6 +83,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
$("#submit-lang").click(function(event, xhr) {
|
||||
event.preventDefault();
|
||||
$.post('/lang_pref/setlang/',
|
||||
{"language": $('#settings-language-value').val()})
|
||||
.done(
|
||||
function(data){
|
||||
// submit form as normal
|
||||
$('.settings-language-form').submit();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#change_email_form").submit(function(){
|
||||
var new_email = $('#new_email_field').val();
|
||||
var new_password = $('#new_email_password').val();
|
||||
@@ -193,6 +206,10 @@
|
||||
</span> <span class="data">${ user.email | h }</span>
|
||||
</li>
|
||||
|
||||
%if len(language_options) > 1:
|
||||
<%include file='dashboard/_dashboard_info_language.html' />
|
||||
%endif
|
||||
|
||||
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
|
||||
<li class="controls--account">
|
||||
<span class="title"><div class="icon"></div><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span>
|
||||
@@ -262,10 +279,21 @@
|
||||
|
||||
<section id="email-settings-modal" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="email-settings-title">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2 id="email-settings-title">${_('Email Settings for {course_number}').format(course_number='<span id="email_settings_course_number"></span>')}<span class="sr">, ${_("modal open")}</span></h2>
|
||||
<h2 id="email-settings-title">
|
||||
${_('Email Settings for {course_number}').format(course_number='<span id="email_settings_course_number"></span>')}
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("modal open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
|
||||
@@ -283,10 +311,21 @@
|
||||
|
||||
<section id="password_reset_complete" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="password-reset-email">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2 id="password-reset-email">${_('Password Reset Email Sent')}<span class="sr">, ${_("modal open")}</span></h2>
|
||||
<h2 id="password-reset-email">
|
||||
${_('Password Reset Email Sent')}
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("modal open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div>
|
||||
@@ -301,10 +340,21 @@
|
||||
|
||||
<section id="change_email" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="change_email_title">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2><span id="change_email_title">${_("Change Email")}</span><span class="sr">, ${_("modal open")}</span></h2>
|
||||
<h2>
|
||||
<span id="change_email_title">${_("Change Email")}</span>
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("modal open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="change_email_body">
|
||||
@@ -329,12 +379,25 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%include file='modal/_modal-settings-language.html' />
|
||||
|
||||
<section id="apply_name_change" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="change-name-title">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2 id="change-name-title">${_("Change your name")}<span class="sr">, ${_("modal open")}</span></h2>
|
||||
<h2 id="change-name-title">
|
||||
${_("Change your name")}
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("modal open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="change_name_body">
|
||||
@@ -360,9 +423,21 @@
|
||||
|
||||
<section id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="alertdialog" aria-labelledy="unenrollment-modal-title">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2 id="unenrollment-modal-title">${_('<span id="track-info"></span> {course_number}? <span id="refund-info"></span>').format(course_number='<span id="unenroll_course_number"></span>')}<span class="sr">, ${_("modal open")}</span></h2>
|
||||
<h2 id="unenrollment-modal-title">
|
||||
${_('<span id="track-info"></span> {course_number}? <span id="refund-info"></span>').format(course_number='<span id="unenroll_course_number"></span>')}
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("modal open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="unenroll_error" class="modal-form-error"></div>
|
||||
|
||||
12
lms/templates/dashboard/_dashboard_info_language.html
Normal file
12
lms/templates/dashboard/_dashboard_info_language.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<li class="status status-language">
|
||||
<span class="title status-title">
|
||||
<i class="icon icon-flag-alt"></i>
|
||||
${_("Preferred Language")}
|
||||
(<a href="#change_language" rel="leanModal" class="edit-language">${_("edit")}</a>)
|
||||
</span>
|
||||
<span class="data">${current_language}</span>
|
||||
</li>
|
||||
@@ -4,7 +4,12 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<section id="forgot-password-modal" class="modal" role="dialog" aria-label="${_('Password Reset')}">
|
||||
<div class="inner-wrapper">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div id="password-reset">
|
||||
<header>
|
||||
|
||||
@@ -16,7 +16,13 @@
|
||||
<section id="help-modal" class="modal" aria-hidden="true" role="dialog" aria-label="${_("{platform_name} Help").format(platform_name=MicrositeConfiguration.get_microsite_configuration_value("platform_name", settings.PLATFORM_NAME))}">
|
||||
<div class="inner-wrapper" id="help_wrapper">
|
||||
## TODO: find a way to refactor this
|
||||
<button class="close-modal "tabindex="0">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal "tabindex="0">
|
||||
✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2>${_('{span_start}{platform_name}{span_end} Help').format(span_start='<span class="edx">', span_end='</span>', platform_name=MicrositeConfiguration.get_microsite_configuration_value('platform_name', settings.PLATFORM_NAME))}</h2>
|
||||
@@ -54,7 +60,13 @@ discussion_link = get_discussion_link(course) if course else None
|
||||
</div>
|
||||
|
||||
<div class="inner-wrapper" id="feedback_form_wrapper">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">
|
||||
✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header></header>
|
||||
|
||||
@@ -82,7 +94,13 @@ discussion_link = get_discussion_link(course) if course else None
|
||||
</div>
|
||||
|
||||
<div class="inner-wrapper" id="feedback_success_wrapper" tabindex="0">
|
||||
<button class="close-modal "tabindex="0">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal" tabindex="0">
|
||||
✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2>${_('Thank You!')}</h2>
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
|
||||
<section id="login-modal" class="modal login-modal">
|
||||
<div class="inner-wrapper">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2>${_("Log In")}</h2>
|
||||
|
||||
61
lms/templates/modal/_modal-settings-language.html
Normal file
61
lms/templates/modal/_modal-settings-language.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<section id="change_language" class="modal modal-settings-language" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="change_language_title">
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<header>
|
||||
<h2>
|
||||
<span id="change_language_title">${_("Change Preferred Language")}</span>
|
||||
<span class="sr">,
|
||||
## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not
|
||||
${_("modal open")}
|
||||
</span>
|
||||
</h2>
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="change_language_body">
|
||||
<form action="/i18n/setlang/" method="post" class="settings-language-form" id="settings-form">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
<ol class="list-input">
|
||||
<li class="field text settings-language-select" id="settings-language-select">
|
||||
<label class="label sr" for="settings-language-value">${_("Please choose your preferred language")}</label>
|
||||
<select class="input select" id="settings-language-value" name="language">
|
||||
% for abbrv in language_options:
|
||||
% for language in settings.LANGUAGES:
|
||||
% if abbrv == language[0]:
|
||||
% if abbrv == current_language_code:
|
||||
<option value="${language[0]}" selected="selected">${language[1]}</option>
|
||||
% else:
|
||||
<option value="${language[0]}">${language[1]}</option>
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
% endfor
|
||||
</select>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="submit">
|
||||
<input type="submit" id="submit-lang" value="${_('Save Language Settings')}" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul class="list list-actions actions-supplemental">
|
||||
<li class="list-actions-item">
|
||||
${_("Don't see your preferred language? {link_start}Volunteer to become a translator!{link_end}").format(link_start='<a class=" action action-volunteer" rel="external" target="_blank" href="https://github.com/edx/edx-platform/blob/master/docs/en_us/developers/source/i18n_translators_guide.rst">', link_end="</a>")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -10,7 +10,12 @@
|
||||
|
||||
<section id="signup-modal" class="modal signup-modal">
|
||||
<div class="inner-wrapper">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<button class="close-modal">✕
|
||||
<span class="sr">
|
||||
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
|
||||
${_('Close Modal')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div id="register">
|
||||
<header>
|
||||
|
||||
@@ -61,7 +61,11 @@ urlpatterns = ('', # nopep8
|
||||
|
||||
url(r'^user_api/', include('user_api.urls')),
|
||||
|
||||
url(r'^lang_pref/', include('lang_pref.urls')),
|
||||
|
||||
url(r'^', include('waffle.urls')),
|
||||
|
||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||
)
|
||||
|
||||
# if settings.FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace :'test:bok_choy' do
|
||||
# Clear any test data already in Mongo or MySQL and invalidate the cache
|
||||
clear_mongo()
|
||||
BOK_CHOY_CACHE.flush()
|
||||
sh(django_admin('lms', 'bok_choy', 'flush', '--noinput'))
|
||||
sh(django_admin('lms', 'bok_choy', 'loaddata', 'common/test/db_fixtures/*.json'))
|
||||
|
||||
# Ensure the test servers are available
|
||||
puts "Starting test servers...".green
|
||||
|
||||
Reference in New Issue
Block a user