Commit 64f1cae6 by Julia Hansbrough

Merge pull request #2419 from edx/talbs/lms-languagemenu

LMS: Adds Language Selection Menu
parents d6f1ddd8 ff0a6fb7
...@@ -456,8 +456,13 @@ INSTALLED_APPS = ( ...@@ -456,8 +456,13 @@ INSTALLED_APPS = (
# Dark-launching languages # Dark-launching languages
'dark_lang', 'dark_lang',
# Student identity reverification # Student identity reverification
'reverification', 'reverification',
# User preferences
'user_api',
'django_openid_auth',
) )
......
...@@ -36,6 +36,9 @@ urlpatterns = patterns('', # nopep8 ...@@ -36,6 +36,9 @@ urlpatterns = patterns('', # nopep8
url(r'^xmodule/', include('pipeline_js.urls')), url(r'^xmodule/', include('pipeline_js.urls')),
url(r'^heartbeat$', include('heartbeat.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 # User creation and updating views
......
...@@ -19,6 +19,8 @@ class DarkLangConfig(ConfigurationModel): ...@@ -19,6 +19,8 @@ class DarkLangConfig(ConfigurationModel):
def released_languages_list(self): def released_languages_list(self):
""" """
``released_languages`` as a list of language codes. ``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 if not self.released_languages.strip(): # pylint: disable=no-member
return [] return []
......
"""
Useful information for setting the language preference
"""
# this is the UserPreference key for the user's preferred language
LANGUAGE_KEY = 'pref-lang'
"""
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
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')
"""
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))
"""
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')
)
"""
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 ...@@ -45,6 +45,7 @@ from student.firebase_token_generator import create_token
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
from dark_lang.models import DarkLangConfig
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
...@@ -61,6 +62,8 @@ import external_auth.views ...@@ -61,6 +62,8 @@ import external_auth.views
from bulk_email.models import Optout, CourseAuthorization from bulk_email.models import Optout, CourseAuthorization
import shoppingcart import shoppingcart
from user_api.models import UserPreference
from lang_pref import LANGUAGE_KEY
import track.views import track.views
...@@ -468,23 +471,42 @@ def dashboard(request): ...@@ -468,23 +471,42 @@ def dashboard(request):
# we'll display the banner # we'll display the banner
denied_banner = any(item.display for item in reverifications["denied"]) denied_banner = any(item.display for item in reverifications["denied"])
context = {'course_enrollment_pairs': course_enrollment_pairs, language_options = DarkLangConfig.current().released_languages_list
'course_optouts': course_optouts,
'message': message, # add in the default language if it's not in the list of released languages
'external_auth_map': external_auth_map, if settings.LANGUAGE_CODE not in language_options:
'staff_access': staff_access, language_options.append(settings.LANGUAGE_CODE)
'errored_courses': errored_courses,
'show_courseware_links_for': show_courseware_links_for, # try to get the prefered language for the user
'all_course_modes': course_modes, cur_lang_code = UserPreference.get_preference(request.user, LANGUAGE_KEY)
'cert_statuses': cert_statuses, if cur_lang_code:
'show_email_settings_for': show_email_settings_for, # if the user has a preference, get the name from the code
'reverifications': reverifications, current_language = settings.LANGUAGE_DICT[cur_lang_code]
'verification_status': verification_status, else:
'verification_msg': verification_msg, # if the user doesn't have a preference, use the default language
'show_refund_option_for': show_refund_option_for, current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
'denied_banner': denied_banner,
'billing_email': settings.PAYMENT_SUPPORT_EMAIL, 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) return render_to_response('dashboard.html', context)
......
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 ...@@ -2,6 +2,7 @@ from django.db import IntegrityError
from django.test import TestCase from django.test import TestCase
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from user_api.tests.factories import UserPreferenceFactory from user_api.tests.factories import UserPreferenceFactory
from user_api.models import UserPreference
class UserPreferenceModelTest(TestCase): class UserPreferenceModelTest(TestCase):
...@@ -26,3 +27,21 @@ class UserPreferenceModelTest(TestCase): ...@@ -26,3 +27,21 @@ class UserPreferenceModelTest(TestCase):
key="testkey3", key="testkey3",
value="\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\xad\xe5\x9b\xbd\xe6\x96\x87\xe5\xad\x97'" 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)
import base64 import base64
from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
import json import json
...@@ -17,21 +16,9 @@ USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/" ...@@ -17,21 +16,9 @@ USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/"
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
class UserApiTestCase(TestCase): class ApiTestCase(TestCase):
def setUp(self):
super(UserApiTestCase, self).setUp() LIST_URI = USER_LIST_URI
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")
]
def basic_auth(self, username, password): def basic_auth(self, username, password):
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))} return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
...@@ -95,11 +82,41 @@ class UserApiTestCase(TestCase): ...@@ -95,11 +82,41 @@ class UserApiTestCase(TestCase):
"""Assert that the given response has the status code 403""" """Assert that the given response has the status code 403"""
self.assertEqual(response.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): def assertHttpMethodNotAllowed(self, response):
"""Assert that the given response has the status code 405""" """Assert that the given response has the status code 405"""
self.assertEqual(response.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): class UserViewSetTest(UserApiTestCase):
LIST_URI = USER_LIST_URI LIST_URI = USER_LIST_URI
...@@ -137,17 +154,10 @@ class UserViewSetTest(UserApiTestCase): ...@@ -137,17 +154,10 @@ class UserViewSetTest(UserApiTestCase):
def test_basic_auth(self): def test_basic_auth(self):
# ensure that having basic auth headers in the mix does not break anything # ensure that having basic auth headers in the mix does not break anything
self.assertHttpOK( 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.assertHttpForbidden(
self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass'))) 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"], [])
def test_get_list_nonempty(self): def test_get_list_nonempty(self):
result = self.get_json(self.LIST_URI) result = self.get_json(self.LIST_URI)
...@@ -245,14 +255,6 @@ class UserPreferenceViewSetTest(UserApiTestCase): ...@@ -245,14 +255,6 @@ class UserPreferenceViewSetTest(UserApiTestCase):
def test_debug_auth(self): def test_debug_auth(self):
self.assertHttpOK(self.client.get(self.LIST_URI)) 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): def test_get_list_nonempty(self):
result = self.get_json(self.LIST_URI) result = self.get_json(self.LIST_URI)
self.assertEqual(result["count"], 3) self.assertEqual(result["count"], 3)
......
...@@ -4,8 +4,8 @@ from rest_framework import authentication ...@@ -4,8 +4,8 @@ from rest_framework import authentication
from rest_framework import filters from rest_framework import filters
from rest_framework import permissions from rest_framework import permissions
from rest_framework import viewsets from rest_framework import viewsets
from user_api.models import UserPreference
from user_api.serializers import UserSerializer, UserPreferenceSerializer from user_api.serializers import UserSerializer, UserPreferenceSerializer
from user_api.models import UserPreference
class ApiKeyHeaderPermission(permissions.BasePermission): class ApiKeyHeaderPermission(permissions.BasePermission):
......
# -*- coding: utf-8 -*-
""" """
Student dashboard page. Student dashboard page.
""" """
...@@ -18,6 +19,14 @@ class DashboardPage(PageObject): ...@@ -18,6 +19,14 @@ class DashboardPage(PageObject):
return self.is_css_present('section.my-courses') return self.is_css_present('section.my-courses')
@property @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): def available_courses(self):
""" """
Return list of the names of available courses (e.g. "999 edX Demonstration Course") Return list of the names of available courses (e.g. "999 edX Demonstration Course")
...@@ -59,3 +68,11 @@ class DashboardPage(PageObject): ...@@ -59,3 +68,11 @@ class DashboardPage(PageObject):
return "a.enter-course:nth-of-type({0})".format(link_index + 1) return "a.enter-course:nth-of-type({0})".format(link_index + 1)
else: else:
return None 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")
# -*- coding: utf-8 -*-
""" """
E2E tests for the LMS. E2E tests for the LMS.
""" """
...@@ -5,7 +6,7 @@ E2E tests for the LMS. ...@@ -5,7 +6,7 @@ E2E tests for the LMS.
from unittest import skip from unittest import skip
from bok_choy.web_app_test import WebAppTest 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 .helpers import UniqueCourseTest, load_data_str
from ..pages.studio.auto_auth import AutoAuthPage from ..pages.studio.auto_auth import AutoAuthPage
...@@ -17,6 +18,7 @@ from ..pages.lms.course_info import CourseInfoPage ...@@ -17,6 +18,7 @@ from ..pages.lms.course_info import CourseInfoPage
from ..pages.lms.tab_nav import TabNavPage from ..pages.lms.tab_nav import TabNavPage
from ..pages.lms.course_nav import CourseNavPage from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.progress import ProgressPage from ..pages.lms.progress import ProgressPage
from ..pages.lms.dashboard import DashboardPage
from ..pages.lms.video import VideoPage from ..pages.lms.video import VideoPage
from ..pages.xblock.acid import AcidView from ..pages.xblock.acid import AcidView
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
...@@ -68,6 +70,66 @@ class RegistrationTest(UniqueCourseTest): ...@@ -68,6 +70,66 @@ class RegistrationTest(UniqueCourseTest):
self.assertIn(self.course_info['display_name'], course_names) 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): class HighLevelTabTest(UniqueCourseTest):
""" """
Tests that verify each of the high-level tabs available within a course. Tests that verify each of the high-level tabs available within a course.
......
[{"pk": 1, "model": "dark_lang.darklangconfig", "fields": {"change_date": "2100-01-30T20:34:20Z", "changed_by": null, "enabled": true, "released_languages": "en,eo"}}]
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 ...@@ -501,7 +501,8 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# Sourced from http://www.localeplanet.com/icu/ and wikipedia # Sourced from http://www.localeplanet.com/icu/ and wikipedia
LANGUAGES = ( 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) ('fake2', u'Fake translations'), # Another dummy language for testing (not pushed to prod)
('ach', u'Acholi'), # Acoli ('ach', u'Acholi'), # Acoli
...@@ -554,6 +555,8 @@ LANGUAGES = ( ...@@ -554,6 +555,8 @@ LANGUAGES = (
('zh-tw', u'台灣正體'), # Chinese (Taiwan) ('zh-tw', u'台灣正體'), # Chinese (Taiwan)
) )
LANGUAGE_DICT = dict(LANGUAGES)
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
...@@ -698,6 +701,10 @@ MIDDLEWARE_CLASSES = ( ...@@ -698,6 +701,10 @@ MIDDLEWARE_CLASSES = (
# Allows us to dark-launch particular languages # Allows us to dark-launch particular languages
'dark_lang.middleware.DarkLangMiddleware', '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 # Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
......
...@@ -97,6 +97,26 @@ ...@@ -97,6 +97,26 @@
%ui-depth4 { z-index: 10000; } %ui-depth4 { z-index: 10000; }
%ui-depth5 { z-index: 100000; } %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 // extends -hidden elems - screenreaders
%text-sr { %text-sr {
border: 0; border: 0;
......
...@@ -231,7 +231,8 @@ ...@@ -231,7 +231,8 @@
cursor: default; cursor: default;
pointer-events: none; pointer-events: none;
box-shadow: none; box-shadow: none;
:hover, :focus {
&:hover, &:focus {
pointer-events: none; pointer-events: none;
} }
} }
......
...@@ -854,6 +854,22 @@ ...@@ -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
.status-verification { .status-verification {
......
...@@ -325,3 +325,37 @@ ...@@ -325,3 +325,37 @@
@extend .modal; @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);
}
}
}
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from django.template import RequestContext %>
<%! <%!
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -82,6 +83,18 @@ ...@@ -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(){ $("#change_email_form").submit(function(){
var new_email = $('#new_email_field').val(); var new_email = $('#new_email_field').val();
var new_password = $('#new_email_password').val(); var new_password = $('#new_email_password').val();
...@@ -193,6 +206,10 @@ ...@@ -193,6 +206,10 @@
</span> <span class="data">${ user.email | h }</span> </span> <span class="data">${ user.email | h }</span>
</li> </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: % if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
<li class="controls--account"> <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> <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 @@ ...@@ -262,10 +279,21 @@
<section id="email-settings-modal" class="modal" aria-hidden="true"> <section id="email-settings-modal" class="modal" aria-hidden="true">
<div class="inner-wrapper" role="dialog" aria-labelledby="email-settings-title"> <div class="inner-wrapper" role="dialog" aria-labelledby="email-settings-title">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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>
<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/> <hr/>
</header> </header>
...@@ -283,10 +311,21 @@ ...@@ -283,10 +311,21 @@
<section id="password_reset_complete" class="modal" aria-hidden="true"> <section id="password_reset_complete" class="modal" aria-hidden="true">
<div class="inner-wrapper" role="dialog" aria-labelledby="password-reset-email"> <div class="inner-wrapper" role="dialog" aria-labelledby="password-reset-email">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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>
<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/> <hr/>
</header> </header>
<div> <div>
...@@ -301,10 +340,21 @@ ...@@ -301,10 +340,21 @@
<section id="change_email" class="modal" aria-hidden="true"> <section id="change_email" class="modal" aria-hidden="true">
<div class="inner-wrapper" role="dialog" aria-labelledby="change_email_title"> <div class="inner-wrapper" role="dialog" aria-labelledby="change_email_title">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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>
<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/> <hr/>
</header> </header>
<div id="change_email_body"> <div id="change_email_body">
...@@ -329,12 +379,25 @@ ...@@ -329,12 +379,25 @@
</div> </div>
</section> </section>
<%include file='modal/_modal-settings-language.html' />
<section id="apply_name_change" class="modal" aria-hidden="true"> <section id="apply_name_change" class="modal" aria-hidden="true">
<div class="inner-wrapper" role="dialog" aria-labelledby="change-name-title"> <div class="inner-wrapper" role="dialog" aria-labelledby="change-name-title">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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>
<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/> <hr/>
</header> </header>
<div id="change_name_body"> <div id="change_name_body">
...@@ -360,9 +423,21 @@ ...@@ -360,9 +423,21 @@
<section id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true"> <section id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true">
<div class="inner-wrapper" role="alertdialog" aria-labelledy="unenrollment-modal-title"> <div class="inner-wrapper" role="alertdialog" aria-labelledy="unenrollment-modal-title">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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>
<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/> <hr/>
</header> </header>
<div id="unenroll_error" class="modal-form-error"></div> <div id="unenroll_error" class="modal-form-error"></div>
......
<%! 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 @@ ...@@ -4,7 +4,12 @@
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<section id="forgot-password-modal" class="modal" role="dialog" aria-label="${_('Password Reset')}"> <section id="forgot-password-modal" class="modal" role="dialog" aria-label="${_('Password Reset')}">
<div class="inner-wrapper"> <div class="inner-wrapper">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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"> <div id="password-reset">
<header> <header>
......
...@@ -16,7 +16,13 @@ ...@@ -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))}"> <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"> <div class="inner-wrapper" id="help_wrapper">
## TODO: find a way to refactor this ## TODO: find a way to refactor this
<button class="close-modal "tabindex="0">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal "tabindex="0">
&#10005;
<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>
<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> <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 ...@@ -54,7 +60,13 @@ discussion_link = get_discussion_link(course) if course else None
</div> </div>
<div class="inner-wrapper" id="feedback_form_wrapper"> <div class="inner-wrapper" id="feedback_form_wrapper">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">
&#10005;
<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> <header></header>
...@@ -82,7 +94,13 @@ discussion_link = get_discussion_link(course) if course else None ...@@ -82,7 +94,13 @@ discussion_link = get_discussion_link(course) if course else None
</div> </div>
<div class="inner-wrapper" id="feedback_success_wrapper" tabindex="0"> <div class="inner-wrapper" id="feedback_success_wrapper" tabindex="0">
<button class="close-modal "tabindex="0">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal" tabindex="0">
&#10005;
<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>
<h2>${_('Thank You!')}</h2> <h2>${_('Thank You!')}</h2>
......
...@@ -5,7 +5,12 @@ ...@@ -5,7 +5,12 @@
<section id="login-modal" class="modal login-modal"> <section id="login-modal" class="modal login-modal">
<div class="inner-wrapper"> <div class="inner-wrapper">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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>
<h2>${_("Log In")}</h2> <h2>${_("Log In")}</h2>
......
<%! 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">&#10005;
<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 @@ ...@@ -10,7 +10,12 @@
<section id="signup-modal" class="modal signup-modal"> <section id="signup-modal" class="modal signup-modal">
<div class="inner-wrapper"> <div class="inner-wrapper">
<button class="close-modal">&#10005; <span class="sr">${_('Close Modal')}</span></button> <button class="close-modal">&#10005;
<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"> <div id="register">
<header> <header>
......
...@@ -61,7 +61,11 @@ urlpatterns = ('', # nopep8 ...@@ -61,7 +61,11 @@ urlpatterns = ('', # nopep8
url(r'^user_api/', include('user_api.urls')), url(r'^user_api/', include('user_api.urls')),
url(r'^lang_pref/', include('lang_pref.urls')),
url(r'^', include('waffle.urls')), url(r'^', include('waffle.urls')),
url(r'^i18n/', include('django.conf.urls.i18n')),
) )
# if settings.FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"): # if settings.FEATURES.get("MULTIPLE_ENROLLMENT_ROLES"):
......
...@@ -195,7 +195,7 @@ namespace :'test:bok_choy' do ...@@ -195,7 +195,7 @@ namespace :'test:bok_choy' do
# Clear any test data already in Mongo or MySQL and invalidate the cache # Clear any test data already in Mongo or MySQL and invalidate the cache
clear_mongo() clear_mongo()
BOK_CHOY_CACHE.flush() 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 # Ensure the test servers are available
puts "Starting test servers...".green puts "Starting test servers...".green
......
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