diff --git a/cms/envs/common.py b/cms/envs/common.py index b088259065..23b7de10a5 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -456,8 +456,13 @@ INSTALLED_APPS = ( # Dark-launching languages 'dark_lang', + # Student identity reverification 'reverification', + + # User preferences + 'user_api', + 'django_openid_auth', ) diff --git a/cms/urls.py b/cms/urls.py index 8192b45886..389409b834 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -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 diff --git a/common/djangoapps/dark_lang/models.py b/common/djangoapps/dark_lang/models.py index 9912287b4e..6379a21fe2 100644 --- a/common/djangoapps/dark_lang/models.py +++ b/common/djangoapps/dark_lang/models.py @@ -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 [] diff --git a/common/djangoapps/lang_pref/__init__.py b/common/djangoapps/lang_pref/__init__.py new file mode 100644 index 0000000000..4182bb6978 --- /dev/null +++ b/common/djangoapps/lang_pref/__init__.py @@ -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' diff --git a/common/djangoapps/lang_pref/middleware.py b/common/djangoapps/lang_pref/middleware.py new file mode 100644 index 0000000000..b14ea33690 --- /dev/null +++ b/common/djangoapps/lang_pref/middleware.py @@ -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 diff --git a/common/djangoapps/lang_pref/tests/test_middleware.py b/common/djangoapps/lang_pref/tests/test_middleware.py new file mode 100644 index 0000000000..68d39265b6 --- /dev/null +++ b/common/djangoapps/lang_pref/tests/test_middleware.py @@ -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') diff --git a/common/djangoapps/lang_pref/tests/test_views.py b/common/djangoapps/lang_pref/tests/test_views.py new file mode 100644 index 0000000000..7d6bffd2a9 --- /dev/null +++ b/common/djangoapps/lang_pref/tests/test_views.py @@ -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)) diff --git a/common/djangoapps/lang_pref/urls.py b/common/djangoapps/lang_pref/urls.py new file mode 100644 index 0000000000..b6199fcdcf --- /dev/null +++ b/common/djangoapps/lang_pref/urls.py @@ -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') +) diff --git a/common/djangoapps/lang_pref/views.py b/common/djangoapps/lang_pref/views.py new file mode 100644 index 0000000000..78f99b73e0 --- /dev/null +++ b/common/djangoapps/lang_pref/views.py @@ -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') diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index bb00430da3..24808d7539 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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) diff --git a/lms/djangoapps/user_api/__init__.py b/common/djangoapps/user_api/__init__.py similarity index 100% rename from lms/djangoapps/user_api/__init__.py rename to common/djangoapps/user_api/__init__.py diff --git a/lms/djangoapps/user_api/migrations/0001_initial.py b/common/djangoapps/user_api/migrations/0001_initial.py similarity index 100% rename from lms/djangoapps/user_api/migrations/0001_initial.py rename to common/djangoapps/user_api/migrations/0001_initial.py diff --git a/lms/djangoapps/user_api/migrations/__init__.py b/common/djangoapps/user_api/migrations/__init__.py similarity index 100% rename from lms/djangoapps/user_api/migrations/__init__.py rename to common/djangoapps/user_api/migrations/__init__.py diff --git a/common/djangoapps/user_api/models.py b/common/djangoapps/user_api/models.py new file mode 100644 index 0000000000..77ab4c6c95 --- /dev/null +++ b/common/djangoapps/user_api/models.py @@ -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 diff --git a/lms/djangoapps/user_api/serializers.py b/common/djangoapps/user_api/serializers.py similarity index 100% rename from lms/djangoapps/user_api/serializers.py rename to common/djangoapps/user_api/serializers.py diff --git a/lms/djangoapps/user_api/tests/__init__.py b/common/djangoapps/user_api/tests/__init__.py similarity index 100% rename from lms/djangoapps/user_api/tests/__init__.py rename to common/djangoapps/user_api/tests/__init__.py diff --git a/lms/djangoapps/user_api/tests/factories.py b/common/djangoapps/user_api/tests/factories.py similarity index 100% rename from lms/djangoapps/user_api/tests/factories.py rename to common/djangoapps/user_api/tests/factories.py diff --git a/lms/djangoapps/user_api/tests/test_models.py b/common/djangoapps/user_api/tests/test_models.py similarity index 61% rename from lms/djangoapps/user_api/tests/test_models.py rename to common/djangoapps/user_api/tests/test_models.py index db1af152b7..b187cd2360 100644 --- a/lms/djangoapps/user_api/tests/test_models.py +++ b/common/djangoapps/user_api/tests/test_models.py @@ -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) diff --git a/lms/djangoapps/user_api/tests/test_views.py b/common/djangoapps/user_api/tests/test_views.py similarity index 95% rename from lms/djangoapps/user_api/tests/test_views.py rename to common/djangoapps/user_api/tests/test_views.py index 451b167050..4143a467d7 100644 --- a/lms/djangoapps/user_api/tests/test_views.py +++ b/common/djangoapps/user_api/tests/test_views.py @@ -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) diff --git a/lms/djangoapps/user_api/urls.py b/common/djangoapps/user_api/urls.py similarity index 100% rename from lms/djangoapps/user_api/urls.py rename to common/djangoapps/user_api/urls.py diff --git a/lms/djangoapps/user_api/views.py b/common/djangoapps/user_api/views.py similarity index 100% rename from lms/djangoapps/user_api/views.py rename to common/djangoapps/user_api/views.py index c64a5a4d23..23066920d8 100644 --- a/lms/djangoapps/user_api/views.py +++ b/common/djangoapps/user_api/views.py @@ -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): diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index 0f768af60d..cd6aa199a8 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -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") diff --git a/common/test/acceptance/tests/test_lms.py b/common/test/acceptance/tests/test_lms.py index 4f65729895..13d82cff4a 100644 --- a/common/test/acceptance/tests/test_lms.py +++ b/common/test/acceptance/tests/test_lms.py @@ -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. diff --git a/common/test/db_fixtures/released_lang.json b/common/test/db_fixtures/released_lang.json new file mode 100644 index 0000000000..7c6f1d0d42 --- /dev/null +++ b/common/test/db_fixtures/released_lang.json @@ -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"}}] diff --git a/lms/djangoapps/user_api/models.py b/lms/djangoapps/user_api/models.py deleted file mode 100644 index 3450c03aa3..0000000000 --- a/lms/djangoapps/user_api/models.py +++ /dev/null @@ -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") diff --git a/lms/envs/common.py b/lms/envs/common.py index a3879e15f6..69b9440e15 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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', diff --git a/lms/static/sass/base/_mixins.scss b/lms/static/sass/base/_mixins.scss index 22a1d96dbd..0205fe9875 100644 --- a/lms/static/sass/base/_mixins.scss +++ b/lms/static/sass/base/_mixins.scss @@ -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; diff --git a/lms/static/sass/elements/_controls.scss b/lms/static/sass/elements/_controls.scss index 30629537da..d7f1376d93 100644 --- a/lms/static/sass/elements/_controls.scss +++ b/lms/static/sass/elements/_controls.scss @@ -231,7 +231,8 @@ cursor: default; pointer-events: none; box-shadow: none; - :hover, :focus { + + &:hover, &:focus { pointer-events: none; } } diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index ef864cb392..5172981661 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -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 { diff --git a/lms/static/sass/shared/_modal.scss b/lms/static/sass/shared/_modal.scss index f9c3027ddf..b8a02d686d 100644 --- a/lms/static/sass/shared/_modal.scss +++ b/lms/static/sass/shared/_modal.scss @@ -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); + } + } +} + diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 47e2abd810..dfccbe476b 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -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 @@ ${ user.email | h } + %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: