diff --git a/common/djangoapps/lang_pref/api.py b/common/djangoapps/lang_pref/api.py index 18b98c1e5a..57ceb3c3f4 100644 --- a/common/djangoapps/lang_pref/api.py +++ b/common/djangoapps/lang_pref/api.py @@ -69,13 +69,15 @@ def preferred_language(preferred_language_code): if preferred_language_code in settings.LANGUAGE_DICT: # If the user has indicated a preference for a valid # language, record their preferred language - preferred_language = settings.LANGUAGE_DICT[preferred_language_code] + pass elif active_language_code in settings.LANGUAGE_DICT: # Otherwise, set the language used in the current thread # as the preferred language - preferred_language = settings.LANGUAGE_DICT[active_language_code] + preferred_language_code = active_language_code else: # Otherwise, use the default language - preferred_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE] + preferred_language_code = settings.LANGUAGE_CODE - return preferred_language + preferred_language = settings.LANGUAGE_DICT[preferred_language_code] + + return Language(preferred_language_code, preferred_language) diff --git a/common/djangoapps/lang_pref/tests/test_api.py b/common/djangoapps/lang_pref/tests/test_api.py index 2362fbc0ea..79f9c0c0da 100644 --- a/common/djangoapps/lang_pref/tests/test_api.py +++ b/common/djangoapps/lang_pref/tests/test_api.py @@ -18,13 +18,13 @@ class LanguageApiTest(TestCase): def test_preferred_language(self): preferred_language = language_api.preferred_language('fr') - self.assertEqual(preferred_language, u'Français') + self.assertEqual(preferred_language, language_api.Language('fr', u'Français')) @ddt.data(*INVALID_LANGUAGE_CODES) def test_invalid_preferred_language(self, language_code): preferred_language = language_api.preferred_language(language_code) - self.assertEqual(preferred_language, u'English') + self.assertEqual(preferred_language, language_api.Language('en', u'English')) def test_no_preferred_language(self): preferred_language = language_api.preferred_language(None) - self.assertEqual(preferred_language, u'English') + self.assertEqual(preferred_language, language_api.Language('en', u'English')) diff --git a/common/djangoapps/user_api/api/profile.py b/common/djangoapps/user_api/api/profile.py index b985d3c039..fc14e47273 100644 --- a/common/djangoapps/user_api/api/profile.py +++ b/common/djangoapps/user_api/api/profile.py @@ -6,6 +6,7 @@ email address. """ + from user_api.models import User, UserProfile, UserPreference from user_api.helpers import intercept_errors diff --git a/common/djangoapps/user_api/tests/test_account_api.py b/common/djangoapps/user_api/tests/test_account_api.py index c343749a6f..1bcc1bbce8 100644 --- a/common/djangoapps/user_api/tests/test_account_api.py +++ b/common/djangoapps/user_api/tests/test_account_api.py @@ -36,6 +36,8 @@ class AccountApiTest(TestCase): "@", "@domain.com", "test@no_extension", + u"fŕáńḱ@example.com", + u"frank@éxáḿṕĺé.ćőḿ", # Long email -- subtract the length of the @domain # except for one character (so we exceed the max length limit) diff --git a/conf/locale/eo/LC_MESSAGES/django.mo b/conf/locale/eo/LC_MESSAGES/django.mo index b333f49827..68bfbd2889 100644 Binary files a/conf/locale/eo/LC_MESSAGES/django.mo and b/conf/locale/eo/LC_MESSAGES/django.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/django.po b/conf/locale/eo/LC_MESSAGES/django.po index 1b2866887d..8e0390e647 100644 --- a/conf/locale/eo/LC_MESSAGES/django.po +++ b/conf/locale/eo/LC_MESSAGES/django.po @@ -37,8 +37,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2014-09-24 14:16-0400\n" -"PO-Revision-Date: 2014-09-24 18:16:48.297294\n" +"POT-Creation-Date: 2014-10-01 13:57+0000\n" +"PO-Revision-Date: 2014-10-01 13:57:56.152525\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -111,7 +111,7 @@ msgstr "Réqüéstéd pägé müst ßé gréätér thän zérö Ⱡ'σяєм ι msgid "Honor Code Certificate" msgstr "Hönör Çödé Çértïfïçäté Ⱡ'σяє#" -#: common/djangoapps/course_modes/views.py common/djangoapps/student/views.py +#: common/djangoapps/course_modes/views.py msgid "Enrollment is closed" msgstr "Énröllmént ïs çlöséd Ⱡ'σя#" @@ -216,14 +216,9 @@ msgstr "Ìnvälïd çöürsé ïd Ⱡ'σ#" msgid "Course id is invalid" msgstr "Çöürsé ïd ïs ïnvälïd Ⱡ'σя#" -#: common/djangoapps/student/views.py -#: lms/templates/courseware/course_about.html -msgid "Course is full" -msgstr "Çöürsé ïs füll Ⱡ'#" - -#: common/djangoapps/student/views.py -msgid "Student is already enrolled" -msgstr "Stüdént ïs älréädý énrölléd Ⱡ'σяєм#" +#: common/djangoapps/student/views.py common/djangoapps/student/views.py +msgid "Could not enroll" +msgstr "Çöüld nöt énröll Ⱡ'σ#" #: common/djangoapps/student/views.py msgid "You are not enrolled in this course" @@ -2794,6 +2789,58 @@ msgstr "" "séttïng hïdés thé Läünçh ßüttön änd äný ÌFrämés för thïs çömpönént. Ⱡ'σяєм " "ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυ#" +#: common/lib/xmodule/xmodule/lti_module.py +msgid "Request user's username" +msgstr "Réqüést üsér's üsérnämé Ⱡ'σяє#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "" +"Select True to request the user's username. You must also set Open in New " +"Page to True to get the user's information." +msgstr "" +"Séléçt Trüé tö réqüést thé üsér's üsérnämé. Ýöü müst älsö sét Öpén ïn Néw " +"Pägé tö Trüé tö gét thé üsér's ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢т#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "Request user's email" +msgstr "Réqüést üsér's émäïl Ⱡ'σя#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "" +"Select True to request the user's email address. You must also set Open in " +"New Page to True to get the user's information." +msgstr "" +"Séléçt Trüé tö réqüést thé üsér's émäïl äddréss. Ýöü müst älsö sét Öpén ïn " +"Néw Pägé tö Trüé tö gét thé üsér's ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт," +" ¢σηѕє¢тєт#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "LTI Application Information" +msgstr "LTÌ Àpplïçätïön Ìnförmätïön Ⱡ'σяєм#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "" +"Enter a description of the third party application. If requesting username " +"and/or email, use this text box to inform users why their username and/or " +"email will be forwarded to a third party application." +msgstr "" +"Éntér ä désçrïptïön öf thé thïrd pärtý äpplïçätïön. Ìf réqüéstïng üsérnämé " +"änd/ör émäïl, üsé thïs téxt ßöx tö ïnförm üsérs whý théïr üsérnämé änd/ör " +"émäïl wïll ßé förwärdéd tö ä thïrd pärtý äpplïçätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт" +" αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "Button Text" +msgstr "Büttön Téxt Ⱡ#" + +#: common/lib/xmodule/xmodule/lti_module.py +msgid "" +"Enter the text on the button used to launch the third party application." +msgstr "" +"Éntér thé téxt ön thé ßüttön üséd tö läünçh thé thïrd pärtý äpplïçätïön. " +"Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" + #: common/lib/xmodule/xmodule/lti_module.py msgid "" "Could not parse custom parameter: {custom_parameter}. Should be \"x=y\" " @@ -4318,6 +4365,10 @@ msgstr "Tïtlé çän't ßé émptý Ⱡ'σя#" msgid "Body can't be empty" msgstr "Bödý çän't ßé émptý Ⱡ'σя#" +#: lms/djangoapps/django_comment_client/base/views.py +msgid "Topic doesn't exist" +msgstr "Töpïç döésn't éxïst Ⱡ'σя#" + #: lms/djangoapps/django_comment_client/base/views.py #: lms/djangoapps/django_comment_client/base/views.py msgid "Comment level too deep" @@ -7689,14 +7740,17 @@ msgstr "édït Ⱡ'σяєм#" #. (for example, Google and LinkedIn) the user can link with or unlink from #. their edX account. #: lms/templates/dashboard.html +#: lms/templates/student_profile/third_party_auth.html msgid "Connected Accounts" msgstr "Çönnéçtéd Àççöünts Ⱡ'σ#" #: lms/templates/dashboard.html +#: lms/templates/student_profile/third_party_auth.html msgid "Linked" msgstr "Lïnkéd Ⱡ'σяєм ιρѕ#" #: lms/templates/dashboard.html +#: lms/templates/student_profile/third_party_auth.html msgid "Not Linked" msgstr "Nöt Lïnkéd Ⱡ#" @@ -7704,6 +7758,7 @@ msgstr "Nöt Lïnkéd Ⱡ#" #. and their account with an external authentication provider (like Google or #. LinkedIn). #: lms/templates/dashboard.html +#: lms/templates/student_profile/third_party_auth.html msgid "Unlink" msgstr "Ûnlïnk Ⱡ'σяєм ιρѕ#" @@ -7711,6 +7766,7 @@ msgstr "Ûnlïnk Ⱡ'σяєм ιρѕ#" #. and their account with an external authentication provider (like Google or #. LinkedIn). #: lms/templates/dashboard.html +#: lms/templates/student_profile/third_party_auth.html msgid "Link" msgstr "Lïnk Ⱡ'σяєм#" @@ -8531,10 +8587,6 @@ msgstr "Çönfïrm #" msgid "Reject" msgstr "Réjéçt Ⱡ'σяєм ιρѕ#" -#: lms/templates/navigation.html lms/templates/original_navigation.html -msgid "Global Navigation" -msgstr "Glößäl Nävïgätïön Ⱡ'σ#" - #: lms/templates/navigation.html lms/templates/navigation.html #: lms/templates/original_navigation.html msgid "Find Courses" @@ -8588,6 +8640,10 @@ msgstr "" msgid "You do not have any notes." msgstr "Ýöü dö nöt hävé äný nötés. Ⱡ'σяєм#" +#: lms/templates/original_navigation.html +msgid "Global Navigation" +msgstr "Glößäl Nävïgätïön Ⱡ'σ#" + #: lms/templates/original_navigation.html #: lms/templates/sysadmin_dashboard.html #: lms/templates/sysadmin_dashboard_gitlogs.html @@ -9602,6 +9658,10 @@ msgstr "" "Àdd {course.display_number_with_default} tö Çärt ({currency_symbol}{cost}) " "Ⱡ'σяє#" +#: lms/templates/courseware/course_about.html +msgid "Course is full" +msgstr "Çöürsé ïs füll Ⱡ'#" + #: lms/templates/courseware/course_about.html msgid "Enrollment in this course is by invitation only" msgstr "Énröllmént ïn thïs çöürsé ïs ßý ïnvïtätïön önlý Ⱡ'σяєм ιρѕυм #" @@ -9827,15 +9887,6 @@ msgstr "" "répört äný prößléms ör döwntïmé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя " "α∂ιριѕι¢ιη#" -#: lms/templates/courseware/grade_summary.html -#: lms/templates/courseware/instructor_dashboard.html -msgid "Grade summary" -msgstr "Grädé sümmärý Ⱡ'#" - -#: lms/templates/courseware/grade_summary.html -msgid "Not implemented yet" -msgstr "Nöt ïmpléméntéd ýét Ⱡ'σя#" - #: lms/templates/courseware/gradebook.html #: lms/templates/courseware/instructor_dashboard.html msgid "Gradebook" @@ -11140,20 +11191,6 @@ msgstr "" "Qüéstïöns räïsé ïssüés thät nééd änswérs. Dïsçüssïöns shäré ïdéäs änd stärt " "çönvérsätïöns. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" -#: lms/templates/discussion/_underscore_templates.html -msgid "Topic Area:" -msgstr "Töpïç Àréä: Ⱡ#" - -#: lms/templates/discussion/_underscore_templates.html -#: lms/templates/discussion/_underscore_templates.html -msgid "Filter topics" -msgstr "Fïltér töpïçs Ⱡ'#" - -#: lms/templates/discussion/_underscore_templates.html -msgid "Add your post to a relevant topic to help others find it." -msgstr "" -"Àdd ýöür pöst tö ä rélévänt töpïç tö hélp öthérs fïnd ït. Ⱡ'σяєм ιρѕυм ∂σł#" - #. Translators: This labels the selector for which group of students can view #. a #. post @@ -11203,6 +11240,20 @@ msgstr "pöst änönýmöüslý tö çlässmätés Ⱡ'σяєм #" msgid "Add Post" msgstr "Àdd Pöst #" +#: lms/templates/discussion/_underscore_templates.html +msgid "Topic Area:" +msgstr "Töpïç Àréä: Ⱡ#" + +#: lms/templates/discussion/_underscore_templates.html +#: lms/templates/discussion/_underscore_templates.html +msgid "Filter topics" +msgstr "Fïltér töpïçs Ⱡ'#" + +#: lms/templates/discussion/_underscore_templates.html +msgid "Add your post to a relevant topic to help others find it." +msgstr "" +"Àdd ýöür pöst tö ä rélévänt töpïç tö hélp öthérs fïnd ït. Ⱡ'σяєм ιρѕυм ∂σł#" + #: lms/templates/discussion/_underscore_templates.html msgid "Endorse" msgstr "Éndörsé #" @@ -13089,10 +13140,12 @@ msgid "None Available" msgstr "Nöné Àväïläßlé Ⱡ'#" #: lms/templates/modal/_modal-settings-language.html +#: lms/templates/student_profile/language.html msgid "Change Preferred Language" msgstr "Çhängé Préférréd Längüägé Ⱡ'σяєм#" #: lms/templates/modal/_modal-settings-language.html +#: lms/templates/student_profile/language.html msgid "Please choose your preferred language" msgstr "Pléäsé çhöösé ýöür préférréd längüägé Ⱡ'σяєм ιρѕ#" @@ -13101,6 +13154,7 @@ msgid "Save Language Settings" msgstr "Sävé Längüägé Séttïngs Ⱡ'σяє#" #: lms/templates/modal/_modal-settings-language.html +#: lms/templates/student_profile/language.html msgid "" "Don't see your preferred language? {link_start}Volunteer to become a " "translator!{link_end}" @@ -13871,6 +13925,50 @@ msgstr "" msgid "Currently the {platform_name} servers are overloaded" msgstr "Çürréntlý thé {platform_name} sérvérs äré övérlöädéd Ⱡ'σяєм ιρѕυ#" +#: lms/templates/student_account/email_change_failed.html +msgid "Email change failed." +msgstr "Émäïl çhängé fäïléd. Ⱡ'σя#" + +#: lms/templates/student_account/email_change_failed.html +msgid "Something went wrong. Please contact {support} for help." +msgstr "" +"Söméthïng wént wröng. Pléäsé çöntäçt {support} för hélp. Ⱡ'σяєм ιρѕυм ∂#" + +#: lms/templates/student_account/email_change_failed.html +msgid "" +"The email address you wanted to use is already used by another " +"{platform_name} account." +msgstr "" +"Thé émäïl äddréss ýöü wäntéd tö üsé ïs älréädý üséd ßý änöthér " +"{platform_name} äççöünt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" + +#: lms/templates/student_account/email_change_failed.html +msgid "" +"You can try again from the {link_start}account settings{link_end} page." +msgstr "" +"Ýöü çän trý ägäïn fröm thé {link_start}äççöünt séttïngs{link_end} pägé. " +"Ⱡ'σяєм ιρѕυм ∂σł#" + +#: lms/templates/student_account/email_change_successful.html +msgid "Email change successful!" +msgstr "Émäïl çhängé süççéssfül! Ⱡ'σяє#" + +#: lms/templates/student_account/email_change_successful.html +msgid "" +"You should see your new email address listed on the {link_start}account " +"settings{link_end} page." +msgstr "" +"Ýöü shöüld séé ýöür néw émäïl äddréss lïstéd ön thé {link_start}äççöünt " +"séttïngs{link_end} pägé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + +#: lms/templates/student_account/index.html +msgid "Student Account" +msgstr "Stüdént Àççöünt Ⱡ'#" + +#: lms/templates/student_profile/index.html +msgid "Student Profile" +msgstr "Stüdént Pröfïlé Ⱡ'#" + #: lms/templates/verify_student/_modal_editname.html msgid "Edit Your Name" msgstr "Édït Ýöür Nämé Ⱡ'#" @@ -13951,11 +14049,13 @@ msgstr "Ýöü äré üpgrädïng ýöür régïsträtïön för Ⱡ'σяєм ι msgid "You are re-verifying for" msgstr "Ýöü äré ré-vérïfýïng för Ⱡ'σяє#" +#: lms/templates/verify_student/_verification_header.html #: lms/templates/verify_student/_verification_header.html #: lms/templates/verify_student/_verification_header.html msgid "You are registering for" msgstr "Ýöü äré régïstérïng för Ⱡ'σяє#" +#: lms/templates/verify_student/_verification_header.html #: lms/templates/verify_student/_verification_header.html msgid "Professional Education" msgstr "Pröféssïönäl Édüçätïön Ⱡ'σяє#" @@ -15235,23 +15335,67 @@ msgstr "Vïéw Lïvé Vérsïön Ⱡ'σ#" msgid "Preview Changes" msgstr "Prévïéw Çhängés Ⱡ'#" -#: cms/templates/container.html cms/templates/group_configurations.html -#: cms/templates/settings_graders.html -msgid "What can I do on this page?" -msgstr "Whät çän Ì dö ön thïs pägé? Ⱡ'σяєм#" +#: cms/templates/container.html +msgid "Adding components" +msgstr "Àddïng çömpönénts Ⱡ'σ#" #: cms/templates/container.html msgid "" -"You can view and edit course components that contain other components on " -"this page. In the case of experiment blocks, this allows you to confirm that" -" you have properly configured your experiment groups and make changes to " -"existing content." +"Select a component type under {em_start}Add New Component{em_end}. Then " +"select a template." msgstr "" -"Ýöü çän vïéw änd édït çöürsé çömpönénts thät çöntäïn öthér çömpönénts ön " -"thïs pägé. Ìn thé çäsé öf éxpérïmént ßlöçks, thïs ällöws ýöü tö çönfïrm thät" -" ýöü hävé pröpérlý çönfïgüréd ýöür éxpérïmént gröüps änd mäké çhängés tö " -"éxïstïng çöntént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт," -" ѕє∂ ∂σ єιυѕмσ∂ тє#" +"Séléçt ä çömpönént týpé ündér {em_start}Àdd Néw Çömpönént{em_end}. Thén " +"séléçt ä témpläté. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт #" + +#: cms/templates/container.html +msgid "" +"The new component is added at the bottom of the page or group. You can then " +"edit and move the component." +msgstr "" +"Thé néw çömpönént ïs äddéd ät thé ßöttöm öf thé pägé ör gröüp. Ýöü çän thén " +"édït änd mövé thé çömpönént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" + +#: cms/templates/container.html +msgid "Editing components" +msgstr "Édïtïng çömpönénts Ⱡ'σ#" + +#: cms/templates/container.html +msgid "" +"Click the {em_start}Edit{em_end} icon in a component to edit its content." +msgstr "" +"Çlïçk thé {em_start}Édït{em_end} ïçön ïn ä çömpönént tö édït ïts çöntént. " +"Ⱡ'σяєм ιρѕυм ∂σłσя#" + +#: cms/templates/container.html +msgid "Reorganizing components" +msgstr "Réörgänïzïng çömpönénts Ⱡ'σяє#" + +#: cms/templates/container.html +msgid "Drag components to new locations within this component." +msgstr "" +"Dräg çömpönénts tö néw löçätïöns wïthïn thïs çömpönént. Ⱡ'σяєм ιρѕυм ∂σł#" + +#: cms/templates/container.html +msgid "For content experiments, you can drag components to other groups." +msgstr "" +"För çöntént éxpérïménts, ýöü çän dräg çömpönénts tö öthér gröüps. Ⱡ'σяєм " +"ιρѕυм ∂σłσя #" + +#: cms/templates/container.html +msgid "Working with content experiments" +msgstr "Wörkïng wïth çöntént éxpérïménts Ⱡ'σяєм ι#" + +#: cms/templates/container.html +msgid "" +"Confirm that you have properly configured content in each of your experiment" +" groups." +msgstr "" +"Çönfïrm thät ýöü hävé pröpérlý çönfïgüréd çöntént ïn éäçh öf ýöür éxpérïmént" +" gröüps. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" + +#: cms/templates/container.html +msgid "Learn more about component containers" +msgstr "Léärn möré äßöüt çömpönént çöntäïnérs Ⱡ'σяєм ιρѕ#" #: cms/templates/container.html msgid "Unit Location" @@ -15920,6 +16064,10 @@ msgstr "Néw Gröüp Çönfïgürätïön Ⱡ'σяє#" msgid "This module is disabled at the moment." msgstr "Thïs mödülé ïs dïsäßléd ät thé mömént. Ⱡ'σяєм ιρѕ#" +#: cms/templates/group_configurations.html cms/templates/settings_graders.html +msgid "What can I do on this page?" +msgstr "Whät çän Ì dö ön thïs pägé? Ⱡ'σяєм#" + #: cms/templates/group_configurations.html msgid "You can create, edit, and delete group configurations." msgstr "" diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.mo b/conf/locale/eo/LC_MESSAGES/djangojs.mo index 56b1b8f521..bec087fa92 100644 Binary files a/conf/locale/eo/LC_MESSAGES/djangojs.mo and b/conf/locale/eo/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.po b/conf/locale/eo/LC_MESSAGES/djangojs.po index 2f182cdff5..9ae8bb4d5b 100644 --- a/conf/locale/eo/LC_MESSAGES/djangojs.po +++ b/conf/locale/eo/LC_MESSAGES/djangojs.po @@ -26,8 +26,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2014-09-24 14:15-0400\n" -"PO-Revision-Date: 2014-09-24 18:16:48.345620\n" +"POT-Creation-Date: 2014-10-01 13:57+0000\n" +"PO-Revision-Date: 2014-10-01 13:57:56.490708\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -1538,6 +1538,36 @@ msgstr "" "Ýöür ßröwsér döésn't süppört dïréçt äççéss tö thé çlïpßöärd. Pléäsé üsé thé " "Çtrl+X/Ç/V kéýßöärd shörtçüts ïnstéäd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" +#: common/lib/xmodule/xmodule/js/src/lti/lti.js +msgid "" +"Click OK to have your username and e-mail address sent to a 3rd party application.\n" +"\n" +"Click Cancel to return to this page without sending your information." +msgstr "" +"Çlïçk ÖK tö hävé ýöür üsérnämé änd é-mäïl äddréss sént tö ä 3rd pärtý äpplïçätïön.\n" +"\n" +"Çlïçk Çänçél tö rétürn tö thïs pägé wïthöüt séndïng ýöür ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#" + +#: common/lib/xmodule/xmodule/js/src/lti/lti.js +msgid "" +"Click OK to have your username sent to a 3rd party application.\n" +"\n" +"Click Cancel to return to this page without sending your information." +msgstr "" +"Çlïçk ÖK tö hävé ýöür üsérnämé sént tö ä 3rd pärtý äpplïçätïön.\n" +"\n" +"Çlïçk Çänçél tö rétürn tö thïs pägé wïthöüt séndïng ýöür ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" + +#: common/lib/xmodule/xmodule/js/src/lti/lti.js +msgid "" +"Click OK to have your e-mail address sent to a 3rd party application.\n" +"\n" +"Click Cancel to return to this page without sending your information." +msgstr "" +"Çlïçk ÖK tö hävé ýöür é-mäïl äddréss sént tö ä 3rd pärtý äpplïçätïön.\n" +"\n" +"Çlïçk Çänçél tö rétürn tö thïs pägé wïthöüt séndïng ýöür ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ι#" + #: common/lib/xmodule/xmodule/js/src/sequence/display.js msgid "" "Sequence error! Cannot navigate to tab %(tab_name)s in the current " @@ -1747,9 +1777,7 @@ msgstr "" #: common/static/coffee/src/discussion/utils.js #: common/static/coffee/src/discussion/views/discussion_thread_list_view.js #: common/static/coffee/src/discussion/views/discussion_thread_list_view.js -#: common/static/coffee/src/discussion/views/new_post_view.js -#: common/static/coffee/src/discussion/views/new_post_view.js -#: common/static/coffee/src/discussion/views/new_post_view.js +#: common/static/coffee/src/discussion/views/discussion_topic_menu_view.js msgid "…" msgstr "… #" @@ -2373,33 +2401,6 @@ msgstr "Çlösé Çälçülätör Ⱡ'σ#" msgid "Post body" msgstr "Pöst ßödý #" -#. Translators: "Distribution" refers to a grade distribution. This error -#. message appears when there is an error getting the data on grade -#. distribution.; -#: lms/static/coffee/src/instructor_dashboard/analytics.js -msgid "Error fetching distribution." -msgstr "Érrör fétçhïng dïstrïßütïön. Ⱡ'σяєм #" - -#: lms/static/coffee/src/instructor_dashboard/analytics.js -#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js -msgid "Unavailable metric display." -msgstr "Ûnäväïläßlé métrïç dïspläý. Ⱡ'σяєм#" - -#: lms/static/coffee/src/instructor_dashboard/analytics.js -#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js -msgid "Error fetching grade distributions." -msgstr "Érrör fétçhïng grädé dïstrïßütïöns. Ⱡ'σяєм ιρ#" - -#: lms/static/coffee/src/instructor_dashboard/analytics.js -#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js -msgid "Last Updated: <%= timestamp %>" -msgstr "Läst Ûpdätéd: <%= timestamp %> Ⱡ'σ#" - -#: lms/static/coffee/src/instructor_dashboard/analytics.js -#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js -msgid "<%= num_students %> students scored." -msgstr "<%= num_students %> stüdénts sçöréd. Ⱡ'σя#" - #: lms/static/coffee/src/instructor_dashboard/data_download.js #: cms/templates/js/mock/mock-group-configuration-page.underscore msgid "Loading..." @@ -2429,6 +2430,22 @@ msgstr "" "Lïnks äré générätéd ön démänd änd éxpïré wïthïn 5 mïnütés düé tö thé " "sénsïtïvé nätüré öf stüdént ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" +#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js +msgid "Unavailable metric display." +msgstr "Ûnäväïläßlé métrïç dïspläý. Ⱡ'σяєм#" + +#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js +msgid "Error fetching grade distributions." +msgstr "Érrör fétçhïng grädé dïstrïßütïöns. Ⱡ'σяєм ιρ#" + +#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js +msgid "Last Updated: <%= timestamp %>" +msgstr "Läst Ûpdätéd: <%= timestamp %> Ⱡ'σ#" + +#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js +msgid "<%= num_students %> students scored." +msgstr "<%= num_students %> stüdénts sçöréd. Ⱡ'σя#" + #: lms/static/coffee/src/instructor_dashboard/membership.js msgid "Username" msgstr "Ûsérnämé #" @@ -3122,6 +3139,31 @@ msgstr "Süççéssfüllý résçöréd prößlém för üsér {user} Ⱡ'σяє msgid "Failed to rescore problem." msgstr "Fäïléd tö résçöré prößlém. Ⱡ'σяєм#" +#: lms/static/js/student_account/account.js +#: lms/static/js/student_profile/profile.js +msgid "The data could not be saved." +msgstr "Thé dätä çöüld nöt ßé sävéd. Ⱡ'σяєм #" + +#: lms/static/js/student_account/account.js +msgid "Please enter a valid email address" +msgstr "Pléäsé éntér ä välïd émäïl äddréss Ⱡ'σяєм ιρ#" + +#: lms/static/js/student_account/account.js +msgid "Please enter a valid password" +msgstr "Pléäsé éntér ä välïd pässwörd Ⱡ'σяєм #" + +#: lms/static/js/student_account/account.js +msgid "Please check your email to confirm the change" +msgstr "Pléäsé çhéçk ýöür émäïl tö çönfïrm thé çhängé Ⱡ'σяєм ιρѕυм#" + +#: lms/static/js/student_profile/profile.js +msgid "Full name cannot be blank" +msgstr "Füll nämé çännöt ßé ßlänk Ⱡ'σяєм#" + +#: lms/static/js/student_profile/profile.js +msgid "Saved" +msgstr "Sävéd Ⱡ'σяєм ι#" + #: lms/templates/class_dashboard/all_section_metrics.js #: lms/templates/class_dashboard/all_section_metrics.js msgid "Unable to retrieve data, please try again later." diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index adb8b8b7e2..0b2447c3ea 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -62,7 +62,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): def test_change_email(self): response = self._change_email(self.NEW_EMAIL, self.PASSWORD) - self.assertEquals(response.status_code, 204) + self.assertEquals(response.status_code, 200) # Verify that the email associated with the account remains unchanged profile_info = profile_api.profile_info(self.USERNAME) @@ -79,7 +79,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): # Retrieve the activation key from the email email_body = mail.outbox[0].body - result = re.search('/email_change_confirm/([^ \n]+)', email_body) + result = re.search('/email/confirmation/([^ \n]+)', email_body) self.assertIsNot(result, None) activation_key = result.group(1) @@ -127,7 +127,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): # Request to change the original user's email to the email used by the inactive user response = self._change_email(self.NEW_EMAIL, self.PASSWORD) - self.assertEquals(response.status_code, 204) + self.assertEquals(response.status_code, 200) @ddt.data(*INVALID_EMAILS) def test_email_change_request_email_invalid(self, invalid_email): @@ -192,14 +192,15 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): self.assertEqual(response.status_code, 400) @ddt.data( - ('get', 'account_index'), - ('put', 'email_change_request') + ('get', 'account_index', []), + ('post', 'email_change_request', []), + ('get', 'email_change_confirm', [123]) ) @ddt.unpack - def test_require_login(self, method, url_name): + def test_require_login(self, method, url_name, args): # Access the page while logged out self.client.logout() - url = reverse(url_name) + url = reverse(url_name, args=args) response = getattr(self.client, method)(url, follow=True) # Should have been redirected to the login page @@ -207,13 +208,14 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): self.assertIn('accounts/login?next=', response.redirect_chain[0][0]) @ddt.data( - ('get', 'account_index'), - ('put', 'email_change_request') + ('get', 'account_index', []), + ('post', 'email_change_request', []), + ('get', 'email_change_confirm', [123]) ) @ddt.unpack - def test_require_http_method(self, correct_method, url_name): + def test_require_http_method(self, correct_method, url_name, args): wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - {correct_method} - url = reverse(url_name) + url = reverse(url_name, args=args) for method in wrong_methods: response = getattr(self.client, method)(url) @@ -230,15 +232,9 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): data = {} if new_email is not None: - data['new_email'] = new_email + data['email'] = new_email if password is not None: # We can't pass a Unicode object to urlencode, so we encode the Unicode object data['password'] = password.encode('utf-8') - response = self.client.put( - path=reverse('email_change_request'), - data=urlencode(data), - content_type='application/x-www-form-urlencoded' - ) - - return response + return self.client.post(path=reverse('email_change_request'), data=data) diff --git a/lms/djangoapps/student_account/urls.py b/lms/djangoapps/student_account/urls.py index 90e6129267..bb5a0d5690 100644 --- a/lms/djangoapps/student_account/urls.py +++ b/lms/djangoapps/student_account/urls.py @@ -3,6 +3,6 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'student_account.views', url(r'^$', 'index', name='account_index'), - url(r'^email_change_request$', 'email_change_request_handler', name='email_change_request'), - url(r'^email_change_confirm/(?P[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'), + url(r'^email$', 'email_change_request_handler', name='email_change_request'), + url(r'^email/confirmation/(?P[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'), ) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index d1344e881a..b20e19d051 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -42,7 +42,7 @@ def index(request): @login_required -@require_http_methods(['PUT']) +@require_http_methods(['POST']) @ensure_csrf_cookie def email_change_request_handler(request): """Handle a request to change the user's email address. @@ -51,7 +51,7 @@ def email_change_request_handler(request): request (HttpRequest) Returns: - HttpResponse: 204 if the confirmation email was sent successfully + HttpResponse: 200 if the confirmation email was sent successfully HttpResponse: 302 if not logged in (redirect to login page) HttpResponse: 400 if the format of the new email is incorrect HttpResponse: 401 if the provided password (in the form) is incorrect @@ -62,22 +62,20 @@ def email_change_request_handler(request): Example usage: - PUT /account/email_change_request + POST /account/email """ - put = QueryDict(request.body) - user = request.user - password = put.get('password') - - username = user.username - old_email = profile_api.profile_info(username)['email'] - new_email = put.get('new_email') + username = request.user.username + password = request.POST.get('password') + new_email = request.POST.get('email') if new_email is None: - return HttpResponseBadRequest("Missing param 'new_email'") + return HttpResponseBadRequest("Missing param 'email'") if password is None: return HttpResponseBadRequest("Missing param 'password'") + old_email = profile_api.profile_info(username)['email'] + try: key = account_api.request_email_change(username, new_email, password) except account_api.AccountUserNotFound: @@ -104,12 +102,11 @@ def email_change_request_handler(request): settings.DEFAULT_FROM_EMAIL ) - # Email new address + # Send a confirmation email to the new address containing the activation key send_mail(subject, message, from_address, [new_email]) - # A 204 is intended to allow input for actions to take place - # without causing a change to the user agent's active document view. - return HttpResponse(status=204) + # Send a 200 response code to the client to indicate that the email was sent successfully. + return HttpResponse(status=200) @login_required diff --git a/lms/djangoapps/student_profile/test/test_views.py b/lms/djangoapps/student_profile/test/test_views.py index c933242c1d..db30146986 100644 --- a/lms/djangoapps/student_profile/test/test_views.py +++ b/lms/djangoapps/student_profile/test/test_views.py @@ -2,7 +2,7 @@ """ Tests for student profile views. """ from urllib import urlencode -from collections import namedtuple +import json from mock import patch import ddt @@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse from util.testing import UrlResetMixin from user_api.api import account as account_api from user_api.api import profile as profile_api -from lang_pref import LANGUAGE_KEY +from lang_pref import LANGUAGE_KEY, api as language_api @ddt.ddt @@ -25,8 +25,7 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): EMAIL = u'walt@savewalterwhite.com' FULL_NAME = u'𝖂𝖆𝖑𝖙𝖊𝖗 𝖂𝖍𝖎𝖙𝖊' - Language = namedtuple('Language', 'code name') - NEW_LANGUAGE = Language('fr', u'Français') + TEST_LANGUAGE = language_api.Language('eo', u'Dummy language') INVALID_LANGUAGE_CODES = [ '', @@ -49,8 +48,6 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): def test_index(self): response = self.client.get(reverse('profile_index')) self.assertContains(response, "Student Profile") - self.assertContains(response, "Change My Name") - self.assertContains(response, "Change Preferred Language") self.assertContains(response, "Connected Accounts") def test_name_change(self): @@ -81,45 +78,61 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): response = self._change_name(self.FULL_NAME) self.assertEqual(response.status_code, 500) + @patch('student_profile.views.language_api.preferred_language') + @patch('student_profile.views.language_api.released_languages') + def test_get_released_languages(self, mock_released_languages, mock_preferred_language): + mock_released_languages.return_value = [self.TEST_LANGUAGE] + mock_preferred_language.return_value = self.TEST_LANGUAGE + + response = self.client.get(reverse('language_info')) + self.assertEqual( + json.loads(response.content), + { + 'preferredLanguage': {'code': self.TEST_LANGUAGE.code, 'name': self.TEST_LANGUAGE.name}, + 'languages': [{'code': self.TEST_LANGUAGE.code, 'name': self.TEST_LANGUAGE.name}] + } + ) + @patch('student_profile.views.language_api.released_languages') def test_language_change(self, mock_released_languages): - mock_released_languages.return_value = [self.NEW_LANGUAGE] + mock_released_languages.return_value = [self.TEST_LANGUAGE] - # Set French as the user's preferred language - response = self._change_language(self.NEW_LANGUAGE.code) + # Set the dummy language as the user's preferred language + response = self._change_preferences(language=self.TEST_LANGUAGE.code) self.assertEqual(response.status_code, 204) - # Verify that French is now the user's preferred language + # Verify that the dummy language is now the user's preferred language preferences = profile_api.preference_info(self.USERNAME) - self.assertEqual(preferences[LANGUAGE_KEY], self.NEW_LANGUAGE.code) + self.assertEqual(preferences[LANGUAGE_KEY], self.TEST_LANGUAGE.code) - # Verify that the page reloads in French + # Verify that the page reloads in the dummy language response = self.client.get(reverse('profile_index')) - self.assertContains(response, "Merci de choisir la langue") + self.assertContains(response, u"Stüdént Pröfïlé") @ddt.data(*INVALID_LANGUAGE_CODES) def test_change_to_invalid_or_unreleased_language(self, language_code): - response = self._change_language(language_code) + response = self._change_preferences(language=language_code) self.assertEqual(response.status_code, 400) def test_change_to_missing_language(self): - response = self._change_language(None) + response = self._change_preferences(language=None) self.assertEqual(response.status_code, 400) @patch('student_profile.views.profile_api.update_preferences') @patch('student_profile.views.language_api.released_languages') def test_language_change_missing_profile(self, mock_released_languages, mock_update_preferences): # This can't happen if the user is logged in, but test it anyway - mock_released_languages.return_value = [self.NEW_LANGUAGE] + mock_released_languages.return_value = [self.TEST_LANGUAGE] mock_update_preferences.side_effect = profile_api.ProfileUserNotFound - response = self._change_language(self.NEW_LANGUAGE.code) + response = self._change_preferences(language=self.TEST_LANGUAGE.code) self.assertEqual(response.status_code, 500) @ddt.data( ('get', 'profile_index'), - ('put', 'name_change'), - ('put', 'language_change') + ('put', 'profile_index'), + ('put', 'preference_handler'), + ('get', 'language_info'), ) @ddt.unpack def test_require_login(self, method, url_name): @@ -133,13 +146,13 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): self.assertIn('accounts/login?next=', response.redirect_chain[0][0]) @ddt.data( - ('get', 'profile_index'), - ('put', 'name_change'), - ('put', 'language_change') + (['get', 'put'], 'profile_index'), + (['put'], 'preference_handler'), + (['get'], 'language_info'), ) @ddt.unpack - def test_require_http_method(self, correct_method, url_name): - wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - {correct_method} + def test_require_http_method(self, correct_methods, url_name): + wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - set(correct_methods) url = reverse(url_name) for method in wrong_methods: @@ -156,27 +169,28 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): data = {} if new_name is not None: # We can't pass a Unicode object to urlencode, so we encode the Unicode object - data['new_name'] = new_name.encode('utf-8') + data['fullName'] = new_name.encode('utf-8') return self.client.put( - path=reverse('name_change'), + path=reverse('profile_index'), data=urlencode(data), content_type='application/x-www-form-urlencoded' ) - def _change_language(self, new_language): - """Request a language change. + def _change_preferences(self, **preferences): + """Request a change to the user's preferences. Returns: HttpResponse """ data = {} - if new_language is not None: - data['new_language'] = new_language + for key, value in preferences.iteritems(): + if value is not None: + data[key] = value return self.client.put( - path=reverse('language_change'), + path=reverse('preference_handler'), data=urlencode(data), content_type='application/x-www-form-urlencoded' ) diff --git a/lms/djangoapps/student_profile/urls.py b/lms/djangoapps/student_profile/urls.py index 81344a0be3..ba4cebeba1 100644 --- a/lms/djangoapps/student_profile/urls.py +++ b/lms/djangoapps/student_profile/urls.py @@ -3,6 +3,6 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'student_profile.views', url(r'^$', 'index', name='profile_index'), - url(r'^name_change$', 'name_change_handler', name='name_change'), - url(r'^language_change$', 'language_change_handler', name='language_change'), + url(r'^preferences$', 'preference_handler', name='preference_handler'), + url(r'^preferences/languages$', 'language_info', name='language_info'), ) diff --git a/lms/djangoapps/student_profile/views.py b/lms/djangoapps/student_profile/views.py index c0ec2f57cd..4ec52f3f40 100644 --- a/lms/djangoapps/student_profile/views.py +++ b/lms/djangoapps/student_profile/views.py @@ -1,14 +1,15 @@ """ Views for a student's profile information. """ -from django.conf import settings +import json + from django.http import ( QueryDict, HttpResponse, HttpResponseBadRequest, HttpResponseServerError ) +from django.conf import settings +from django.views.decorators.http import require_http_methods from django_future.csrf import ensure_csrf_cookie from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_http_methods - from edxmako.shortcuts import render_to_response from user_api.api import profile as profile_api from lang_pref import LANGUAGE_KEY, api as language_api @@ -16,34 +17,47 @@ from third_party_auth import pipeline @login_required -@require_http_methods(['GET']) def index(request): - """Render the profile info page. + """View or modify the student's profile. + + GET: Retrieve the user's profile information. + PUT: Update the user's profile information. Currently the only accept param is "fullName". Args: request (HttpRequest) Returns: - HttpResponse: 200 if successful + HttpResponse: 200 if successful on GET + HttpResponse: 204 if successful on PUT HttpResponse: 302 if not logged in (redirect to login page) + HttpResponse: 400 if the updated information is invalid HttpResponse: 405 if using an unsupported HTTP method + HttpResponse: 500 if an unexpected error occurs. - Example: + """ + if request.method == "GET": + return _get_profile(request) + elif request.method == "PUT": + return _update_profile(request) + else: + return HttpResponse(status=405) - GET /profile + +def _get_profile(request): + """Retrieve the user's profile information, including an HTML form + that students can use to update the information. + + Args: + request (HttpRequest) + + Returns: + HttpResponse """ user = request.user - released_languages = language_api.released_languages() - - preferred_language_code = profile_api.preference_info(user.username).get(LANGUAGE_KEY) - preferred_language = language_api.preferred_language(preferred_language_code) - context = { - 'disable_courseware_js': True, - 'released_languages': released_languages, - 'preferred_language': preferred_language, + 'disable_courseware_js': True } if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): @@ -52,34 +66,24 @@ def index(request): return render_to_response('student_profile/index.html', context) -@login_required -@require_http_methods(['PUT']) @ensure_csrf_cookie -def name_change_handler(request): - """Change the user's name. +def _update_profile(request): + """Update a user's profile information. Args: request (HttpRequest) Returns: - HttpResponse: 204 if successful - HttpResponse: 302 if not logged in (redirect to login page) - HttpResponse: 400 if the provided name is invalid - HttpResponse: 405 if using an unsupported HTTP method - HttpResponse: 500 if an unexpected error occurs. - - Example: - - PUT /profile/name_change + HttpResponse """ put = QueryDict(request.body) username = request.user.username - new_name = put.get('new_name') + new_name = put.get('fullName') if new_name is None: - return HttpResponseBadRequest("Missing param 'new_name'") + return HttpResponseBadRequest("Missing param 'fullName'") try: profile_api.update_profile(username, full_name=new_name) @@ -93,11 +97,48 @@ def name_change_handler(request): return HttpResponse(status=204) +@login_required +@require_http_methods(['GET']) +def language_info(request): + """Retrieve information about languages. + + Gets the user's preferred language and the list of released + languages, encoding the information as JSON. + + Args: + request (HttpRequest) + + Returns: + HttpResponse: 200 if successful on GET + HttpResponse: 302 if not logged in (redirect to login page) + HttpResponse: 405 if using an unsupported HTTP method + HttpResponse: 500 if an unexpected error occurs + + Example: + + GET /profile/preferences/languages + + """ + user = request.user + + preferred_language_code = profile_api.preference_info(user.username).get(LANGUAGE_KEY) + preferred_language = language_api.preferred_language(preferred_language_code) + response_data = {'preferredLanguage': {'code': preferred_language.code, 'name': preferred_language.name}} + + languages = language_api.released_languages() + response_data['languages'] = [{'code': language.code, 'name': language.name} for language in languages] + + return HttpResponse(json.dumps(response_data), content_type='application/json') + + @login_required @require_http_methods(['PUT']) @ensure_csrf_cookie -def language_change_handler(request): - """Change the user's language preference. +def preference_handler(request): + """Change the user's preferences. + + At the moment, the only supported preference is the user's + language choice. Args: request (HttpRequest) @@ -112,16 +153,16 @@ def language_change_handler(request): Example: - PUT /profile/language_change + PUT /profile/preferences """ put = QueryDict(request.body) username = request.user.username - new_language = put.get('new_language') + new_language = put.get('language') if new_language is None: - return HttpResponseBadRequest("Missing param 'new_language'") + return HttpResponseBadRequest("Missing param 'language'") # Check that the provided language code corresponds to a released language released_languages = language_api.released_languages() diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index d30440d4d9..30ce47a9e9 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -212,6 +212,14 @@ }, // LMS class loaded explicitly until they are converted to use RequireJS + 'js/student_account/account': { + exports: 'js/student_account/account', + deps: ['jquery', 'underscore', 'backbone', 'gettext', 'jquery.cookie'] + }, + 'js/student_profile/profile': { + exports: 'js/student_profile/profile', + deps: ['jquery', 'underscore', 'backbone', 'gettext', 'jquery.cookie'] + }, 'js/verify_student/photocapture': { exports: 'js/verify_student/photocapture' }, @@ -261,6 +269,8 @@ 'lms/include/js/spec/staff_debug_actions_spec.js', 'lms/include/js/spec/views/notification_spec.js', 'lms/include/js/spec/dashboard/donation.js', + 'lms/include/js/spec/student_account/account.js', + 'lms/include/js/spec/student_profile/profile.js' ]); }).call(this, requirejs, define); diff --git a/lms/static/js/spec/student_account/account.js b/lms/static/js/spec/student_account/account.js new file mode 100644 index 0000000000..596107f415 --- /dev/null +++ b/lms/static/js/spec/student_account/account.js @@ -0,0 +1,196 @@ +define(['js/student_account/account'], + function() { + describe("edx.student.account.AccountModel", function() { + 'use strict'; + + var account = null; + + var assertValid = function(fields, isValid, expectedErrors) { + account.set(fields); + var errors = account.validate(account.attributes); + + if (isValid) { + expect(errors).toBe(undefined); + } else { + expect(errors).toEqual(expectedErrors); + } + }; + + var EXPECTED_ERRORS = { + email: { + email: "Please enter a valid email address" + }, + password: { + password: "Please enter a valid password" + } + }; + + beforeEach(function() { + account = new edx.student.account.AccountModel(); + account.set({ + email: "bob@example.com", + password: "password" + }); + }); + + it("accepts valid email addresses", function() { + assertValid({email: "bob@example.com"}, true); + assertValid({email: "bob+smith@example.com"}, true); + assertValid({email: "bob+smith@example.com"}, true); + assertValid({email: "bob+smith@example.com"}, true); + assertValid({email: "bob@test.example.com"}, true); + assertValid({email: "bob@test-example.com"}, true); + }); + + it("rejects blank email addresses", function() { + assertValid({email: ""}, false, EXPECTED_ERRORS.email); + assertValid({email: " "}, false, EXPECTED_ERRORS.email); + }); + + it("rejects invalid email addresses", function() { + assertValid({email: "bob"}, false, EXPECTED_ERRORS.email); + assertValid({email: "bob@example"}, false, EXPECTED_ERRORS.email); + assertValid({email: "@"}, false, EXPECTED_ERRORS.email); + assertValid({email: "@example.com"}, false, EXPECTED_ERRORS.email); + + // The server will reject emails with non-ASCII unicode + // Technically these are valid email addresses, but the email validator + // in Django 1.4 will reject them anyway, so we should too. + assertValid({email: "fŕáńḱ@example.com"}, false, EXPECTED_ERRORS.email); + assertValid({email: "frank@éxáḿṕĺé.com"}, false, EXPECTED_ERRORS.email); + }); + + it("rejects a long email address", function() { + // Construct an email exactly one character longer than the maximum length + var longEmail = new Array(account.EMAIL_MAX_LENGTH - 10).join("e") + "@example.com"; + assertValid({email: longEmail}, false, EXPECTED_ERRORS.email); + }); + + it("accepts a valid password", function() { + assertValid({password: "password-test123"}, true, EXPECTED_ERRORS.password); + }); + + it("rejects a short password", function() { + assertValid({password: ""}, false, EXPECTED_ERRORS.password); + assertValid({password: "a"}, false, EXPECTED_ERRORS.password); + assertValid({password: "aa"}, true, EXPECTED_ERRORS.password); + }); + + it("rejects a long password", function() { + // Construct a password exactly one character longer than the maximum length + var longPassword = new Array(account.PASSWORD_MAX_LENGTH + 2).join("a"); + assertValid({password: longPassword}, false, EXPECTED_ERRORS.password); + }); + }); + + + describe("edx.student.account.AccountView", function() { + var view = null, + ajaxSuccess = true; + + var requestEmailChange = function(email, password) { + var fakeEvent = {preventDefault: function() {}}; + view.model.set({ + email: email, + password: password + }); + view.submit(fakeEvent); + }; + + var assertAjax = function(url, method, data) { + expect($.ajax).toHaveBeenCalled(); + var ajaxArgs = $.ajax.mostRecentCall.args[0]; + expect(ajaxArgs.url).toEqual(url); + expect(ajaxArgs.type).toEqual(method); + expect(ajaxArgs.data).toEqual(data); + expect(ajaxArgs.headers.hasOwnProperty("X-CSRFToken")).toBe(true); + }; + + var assertEmailStatus = function(success, expectedStatus) { + if (!success) { + expect(view.$emailStatus).toHaveClass("validation-error"); + } else { + expect(view.$emailStatus).not.toHaveClass("validation-error"); + } + expect(view.$emailStatus.text()).toEqual(expectedStatus); + }; + + var assertPasswordStatus = function(success, expectedStatus) { + if (!success) { + expect(view.$passwordStatus).toHaveClass("validation-error"); + } else { + expect(view.$passwordStatus).not.toHaveClass("validation-error"); + } + expect(view.$passwordStatus.text()).toEqual(expectedStatus); + }; + + var assertRequestStatus = function(success, expectedStatus) { + if (!success) { + expect(view.$requestStatus).toHaveClass("error"); + } else { + expect(view.$requestStatus).not.toHaveClass("error"); + } + expect(view.$requestStatus.text()).toEqual(expectedStatus); + }; + + beforeEach(function() { + var fixture = readFixtures("templates/student_account/account.underscore"); + setFixtures("
" + fixture + "
"); + + view = new edx.student.account.AccountView().render(); + + // Stub Ajax cals to return success/failure + spyOn($, "ajax").andCallFake(function() { + return $.Deferred(function(defer) { + if (ajaxSuccess) { + defer.resolve(); + } else { + defer.reject(); + } + }).promise(); + }); + }); + + it("requests an email address change", function() { + requestEmailChange("bob@example.com", "password"); + assertAjax("email", "POST", { + email: "bob@example.com", + password: "password" + }); + assertRequestStatus(true, "Please check your email to confirm the change"); + }); + + it("displays email validation errors", function() { + // Invalid email should display an error + requestEmailChange("invalid", "password"); + assertEmailStatus(false, "Please enter a valid email address"); + + // Once the error is fixed, the status should return to normal + requestEmailChange("bob@example.com", "password"); + assertEmailStatus(true, ""); + }); + + it("displays an invalid password error", function() { + // Password cannot be empty + requestEmailChange("bob@example.com", ""); + assertPasswordStatus(false, "Please enter a valid password"); + + // Once the error is fixed, the status should return to normal + requestEmailChange("bob@example.com", "password"); + assertPasswordStatus(true, ""); + }); + + it("displays server errors", function() { + // Simulate an error from the server + ajaxSuccess = false; + requestEmailChange("bob@example.com", "password"); + assertRequestStatus(false, "The data could not be saved."); + + // On retry, it should succeed + ajaxSuccess = true; + requestEmailChange("bob@example.com", "password"); + assertRequestStatus(true, "Please check your email to confirm the change"); + }); + }); + } +); diff --git a/lms/static/js/spec/student_profile/profile.js b/lms/static/js/spec/student_profile/profile.js new file mode 100644 index 0000000000..82f354582f --- /dev/null +++ b/lms/static/js/spec/student_profile/profile.js @@ -0,0 +1,178 @@ +define(['js/student_profile/profile'], + function() { + describe("edx.student.profile.ProfileModel", function() { + 'use strict'; + + var profile = null; + + beforeEach(function() { + profile = new edx.student.profile.ProfileModel(); + }); + + it("validates the full name field", function() { + // Full name cannot be blank + profile.set("fullName", ""); + var errors = profile.validate(profile.attributes); + expect(errors).toEqual({ + fullName: "Full name cannot be blank" + }); + + // Fill in the name and expect that the model is valid + profile.set("fullName", "Bob"); + errors = profile.validate(profile.attributes); + expect(errors).toBe(undefined); + }); + }); + + describe("edx.student.profile.PreferencesModel", function() { + var preferences = null; + + beforeEach(function() { + preferences = new edx.student.profile.PreferencesModel(); + }); + + it("validates the language field", function() { + // Language cannot be blank + preferences.set("language", ""); + var errors = preferences.validate(preferences.attributes); + expect(errors).toEqual({ + language: "Language cannot be blank" + }); + + // Fill in the language and expect that the model is valid + preferences.set("language", "eo"); + errors = preferences.validate(preferences.attributes); + expect(errors).toBe(undefined); + }); + }); + + describe("edx.student.profile.ProfileView", function() { + var view = null, + ajaxSuccess = true; + + var updateProfile = function(fields) { + view.profileModel.set(fields); + view.clearStatus(); + view.profileModel.save(); + }; + + var updatePreferences = function(fields) { + view.preferencesModel.set(fields); + view.clearStatus(); + view.preferencesModel.save(); + }; + + var assertAjax = function(url, method, data) { + expect($.ajax).toHaveBeenCalled(); + var ajaxArgs = $.ajax.mostRecentCall.args[0]; + expect(ajaxArgs.url).toEqual(url); + expect(ajaxArgs.type).toEqual(method); + expect(ajaxArgs.data).toEqual(data) + expect(ajaxArgs.headers.hasOwnProperty("X-CSRFToken")).toBe(true); + }; + + var assertSubmitStatus = function(success, expectedStatus) { + if (!success) { + expect(view.$submitStatus).toHaveClass("error"); + } else { + expect(view.$submitStatus).not.toHaveClass("error"); + } + expect(view.$submitStatus.text()).toEqual(expectedStatus); + }; + + var assertValidationError = function(expectedError, selection) { + if (expectedError === null) { + expect(selection).not.toHaveClass("validation-error"); + expect(selection.text()).toEqual(""); + } else { + expect(selection).toHaveClass("validation-error"); + expect(selection.text()).toEqual(expectedError); + } + }; + + beforeEach(function() { + var profileFixture = readFixtures("templates/student_profile/profile.underscore"), + languageFixture = readFixtures("templates/student_profile/languages.underscore"); + + setFixtures("
" + profileFixture + "
"); + appendSetFixtures("
" + languageFixture + "
"); + + // Stub AJAX calls to return success / failure + spyOn($, "ajax").andCallFake(function() { + return $.Deferred(function(defer) { + if (ajaxSuccess) { + defer.resolve(); + } else { + defer.reject(); + } + }).promise(); + }); + + var json = { + preferredLanguage: {code: 'eo', name: 'Dummy language'}, + languages: [{code: 'eo', name: 'Dummy language'}] + }; + spyOn($, "getJSON").andCallFake(function() { + return $.Deferred(function(defer) { + if (ajaxSuccess) { + defer.resolveWith(this, [json]); + } else { + defer.reject(); + } + }).promise(); + }); + + // Stub location.reload() to prevent test suite from reloading repeatedly + spyOn(edx.student.profile, "reloadPage").andCallFake(function() { + return true; + }); + + view = new edx.student.profile.ProfileView().render(); + }); + + it("updates the student profile", function() { + updateProfile({fullName: "John Smith"}); + assertAjax("", "PUT", {fullName: "John Smith"}); + assertSubmitStatus(true, "Saved"); + }); + + it("updates the student preferences", function() { + updatePreferences({language: "eo"}); + assertAjax("preferences", "PUT", {language: "eo"}); + assertSubmitStatus(true, "Saved"); + }); + + it("displays full name validation errors", function() { + // Blank name should display a validation error + updateProfile({fullName: ""}); + assertValidationError("Full name cannot be blank", view.$nameStatus); + + // If we fix the problem and resubmit, the error should go away + updateProfile({fullName: "John Smith"}); + assertValidationError(null, view.$nameStatus); + }); + + it("displays language validation errors", function() { + // Blank language should display a validation error + updatePreferences({language: ""}); + assertValidationError("Language cannot be blank", view.$languageStatus); + + // If we fix the problem and resubmit, the error should go away + updatePreferences({language: "eo"}); + assertValidationError(null, view.$languageStatus); + }); + + it("displays an error if the sync fails", function() { + // If we get an error status on the AJAX request, display an error + ajaxSuccess = false; + updateProfile({fullName: "John Smith"}); + assertSubmitStatus(false, "The data could not be saved."); + + // If we try again and succeed, the error should go away + ajaxSuccess = true; + updateProfile({fullName: "John Smith"}); + assertSubmitStatus(true, "Saved"); + }); + }); + } +); diff --git a/lms/static/js/student_account/account.js b/lms/static/js/student_account/account.js index 172f09db71..dda4fba60f 100644 --- a/lms/static/js/student_account/account.js +++ b/lms/static/js/student_account/account.js @@ -1,141 +1,155 @@ var edx = edx || {}; -(function($) { +(function($, _, Backbone, gettext) { 'use strict'; edx.student = edx.student || {}; + edx.student.account = {}; - edx.student.account = (function() { - var _fn = { - init: function() { - _fn.ajax.init(); - _fn.eventHandlers.init(); - }, + edx.student.account.AccountModel = Backbone.Model.extend({ + // These should be the same length limits enforced by the server + EMAIL_MIN_LENGTH: 3, + EMAIL_MAX_LENGTH: 254, + PASSWORD_MIN_LENGTH: 2, + PASSWORD_MAX_LENGTH: 75, - eventHandlers: { - init: function() { - _fn.eventHandlers.submit(); - }, + // This is the same regex used to validate email addresses in Django 1.4 + EMAIL_REGEX: new RegExp( + "(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" + + '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"' + + ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+[A-Z]{2,6}\\.?$)' + + '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$', + 'i' + ), - submit: function() { - $('#email-change-form').submit( _fn.form.submit ); - } - }, + defaults: { + email: '', + password: '' + }, - ajax: { - init: function() { - var csrftoken = _fn.cookie.get( 'csrftoken' ); + urlRoot: 'email', - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if ( settings.type === 'PUT' ) { - xhr.setRequestHeader( 'X-CSRFToken', csrftoken ); - } - } - }); - }, + sync: function(method, model) { + var headers = { + 'X-CSRFToken': $.cookie('csrftoken') + }; - put: function( url, data ) { - $.ajax({ - url: url, - type: 'PUT', - data: data - }); - } - }, + $.ajax({ + url: model.urlRoot, + type: 'POST', + data: model.attributes, + headers: headers + }) + .done(function() { + model.trigger('sync'); + }) + .fail(function() { + var error = gettext("The data could not be saved."); + model.trigger('error', error); + }); + }, - cookie: { - get: function( name ) { - return $.cookie(name); - } - }, + validate: function(attrs) { + var errors = {}; - form: { - isValid: true, + if (attrs.email.length < this.EMAIL_MIN_LENGTH || + attrs.email.length > this.EMAIL_MAX_LENGTH || + !this.EMAIL_REGEX.test(attrs.email) + ) { errors.email = gettext("Please enter a valid email address"); } - submit: function( event ) { - var $email = $('#new-email'), - $password = $('#password'), - data = { - new_email: $email.val(), - password: $password.val() - }; - - event.preventDefault(); - - _fn.form.validate( $('#email-change-form') ); - - if ( _fn.form.isValid ) { - _fn.ajax.put( 'email_change_request', data ); - } - }, - - validate: function( $form ) { - _fn.form.isValid = true; - $form.find('input').each( _fn.valid.input ); - } - }, - - regex: { - email: function() { - // taken from http://parsleyjs.org/ - return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; - } - }, - - valid: { - email: function( str ) { - var valid = false, - len = str ? str.length : 0, - regex = _fn.regex.email(); - - if ( 0 < len && len < 254 ) { - valid = regex.test( str ); - } - - return valid; - }, - - input: function() { - var $el = $(this), - validation = $el.data('validate'), - value = $el.val(), - valid = true; - - - if ( validation && validation.length > 0 ) { - $el.removeClass('error') - .css('border-color', '#c8c8c8'); // temp. for development - - // Required field - if ( validation.indexOf('required') > -1 ) { - valid = _fn.valid.required( value ); - } - - // Email address - if ( valid && validation.indexOf('email') > -1 ) { - valid = _fn.valid.email( value ); - } - - if ( !valid ) { - $el.addClass('error') - .css('border-color', '#f00'); // temp. for development - _fn.form.isValid = false; - } - } - }, - - required: function( str ) { - return ( str && str.length > 0 ) ? true : false; - } + if (attrs.password.length < this.PASSWORD_MIN_LENGTH || attrs.password.length > this.PASSWORD_MAX_LENGTH) { + errors.password = gettext("Please enter a valid password"); } - }; - return { - init: _fn.init - }; - })(); + if (!$.isEmptyObject(errors)) { + return errors; + } + } + }); - edx.student.account.init(); + edx.student.account.AccountView = Backbone.View.extend({ -})(jQuery); + events: { + 'submit': 'submit', + 'change': 'change' + }, + + initialize: function() { + _.bindAll(this, 'render', 'submit', 'change', 'clearStatus', 'invalid', 'error', 'sync'); + this.model = new edx.student.account.AccountModel(); + this.model.on('invalid', this.invalid); + this.model.on('error', this.error); + this.model.on('sync', this.sync); + }, + + render: function() { + this.$el.html(_.template($('#account-tpl').html(), {})); + this.$email = $('#new-email', this.$el); + this.$password = $('#password', this.$el); + this.$emailStatus = $('#new-email-status', this.$el); + this.$passwordStatus = $('#password-status', this.$el); + this.$requestStatus = $('#request-email-status', this.$el); + return this; + }, + + submit: function(event) { + event.preventDefault(); + this.clearStatus(); + this.model.save(); + }, + + change: function() { + this.model.set({ + email: this.$email.val(), + password: this.$password.val() + }); + }, + + invalid: function(model) { + var errors = model.validationError; + + if (errors.hasOwnProperty('email')) { + this.$emailStatus + .addClass('validation-error') + .text(errors.email); + } + + if (errors.hasOwnProperty('password')) { + this.$passwordStatus + .addClass('validation-error') + .text(errors.password); + } + }, + + error: function(error) { + this.$requestStatus + .addClass('error') + .text(error); + }, + + sync: function() { + this.$requestStatus + .addClass('success') + .text(gettext("Please check your email to confirm the change")); + }, + + clearStatus: function() { + this.$emailStatus + .removeClass('validation-error') + .text(""); + + this.$passwordStatus + .removeClass('validation-error') + .text(""); + + this.$requestStatus + .removeClass('error') + .text(""); + }, + }); + + return new edx.student.account.AccountView({ + el: $('#account-container') + }).render(); + +})(jQuery, _, Backbone, gettext); diff --git a/lms/static/js/student_profile/profile.js b/lms/static/js/student_profile/profile.js index a453dceb45..664b198d65 100644 --- a/lms/static/js/student_profile/profile.js +++ b/lms/static/js/student_profile/profile.js @@ -1,97 +1,205 @@ var edx = edx || {}; -(function($) { +(function($, _, Backbone, gettext) { 'use strict'; edx.student = edx.student || {}; + edx.student.profile = {}; - edx.student.profile = (function() { + var syncErrorMessage = gettext("The data could not be saved."); - var _fn = { - init: function() { - _fn.ajax.init(); - _fn.eventHandlers.init(); - }, + edx.student.profile.reloadPage = function() { + location.reload(); + }; - eventHandlers: { - init: function() { - _fn.eventHandlers.submit(); - _fn.eventHandlers.click(); - }, + edx.student.profile.ProfileModel = Backbone.Model.extend({ + defaults: { + fullName: '' + }, - submit: function() { - $('#name-change-form').on( 'submit', _fn.update.name ); - }, + urlRoot: '', - click: function() { - $('#language-change-form .submit-button').on( 'click', _fn.update.language ); - } - }, + sync: function(method, model) { + var headers = { + 'X-CSRFToken': $.cookie('csrftoken') + }; - update: { - name: function( event ) { - _fn.form.submit( event, '#new-name', 'new_name', 'name_change' ); - }, + $.ajax({ + url: model.urlRoot, + type: 'PUT', + data: model.attributes, + headers: headers + }) + .done(function() { + model.trigger('sync'); + }) + .fail(function() { + model.trigger('error', syncErrorMessage); + }); + }, - language: function( event ) { - /** - * The onSuccess argument here means: take `window.location.reload` - * and return a function that will use `window.location` as the - * `this` reference inside `reload()`. - */ - _fn.form.submit( event, '#new-language', 'new_language', 'language_change', window.location.reload.bind(window.location) ); - } - }, + validate: function(attrs) { + var errors = {}; + if (attrs.fullName.length < 1) { + errors.fullName = gettext("Full name cannot be blank"); + } - form: { - submit: function( event, idSelector, key, url, onSuccess ) { - var $selection = $(idSelector), - data = {}; + if (!$.isEmptyObject(errors)) { + return errors; + } + } + }); - data[key] = $selection.val(); + edx.student.profile.PreferencesModel = Backbone.Model.extend({ + defaults: { + language: 'en' + }, - event.preventDefault(); - _fn.ajax.put( url, data, onSuccess ); - } - }, + urlRoot: 'preferences', - ajax: { - init: function() { - var csrftoken = _fn.cookie.get( 'csrftoken' ); + sync: function(method, model) { + var headers = { + 'X-CSRFToken': $.cookie('csrftoken') + }; - $.ajaxSetup({ - beforeSend: function( xhr, settings ) { - if ( settings.type === 'PUT' ) { - xhr.setRequestHeader( 'X-CSRFToken', csrftoken ); - } - } - }); - }, + $.ajax({ + url: model.urlRoot, + type: 'PUT', + data: model.attributes, + headers: headers + }) + .done(function() { + model.trigger('sync'); + edx.student.profile.reloadPage(); + }) + .fail(function() { + model.trigger('error', syncErrorMessage); + }); + }, - put: function( url, data, onSuccess ) { - $.ajax({ - url: url, - type: 'PUT', - data: data, - success: onSuccess ? onSuccess : '' - }); - } - }, + validate: function(attrs) { + var errors = {}; + if (attrs.language.length < 1) { + errors.language = gettext("Language cannot be blank"); + } - cookie: { - get: function( name ) { - return $.cookie(name); - } - }, + if (!$.isEmptyObject(errors)) { + return errors; + } + } + }); - }; + edx.student.profile.ProfileView = Backbone.View.extend({ - return { - init: _fn.init - }; + events: { + 'submit': 'submit', + 'change': 'change' + }, - })(); + initialize: function() { + _.bindAll(this, 'render', 'change', 'submit', 'invalidProfile', 'invalidPreference', 'error', 'sync', 'clearStatus'); + + this.profileModel = new edx.student.profile.ProfileModel(); + this.profileModel.on('invalid', this.invalidProfile); + this.profileModel.on('error', this.error); + this.profileModel.on('sync', this.sync); - edx.student.profile.init(); + this.preferencesModel = new edx.student.profile.PreferencesModel(); + this.preferencesModel.on('invalid', this.invalidPreference); + this.preferencesModel.on('error', this.error); + this.preferencesModel.on('sync', this.sync); + }, -})(jQuery); + render: function() { + this.$el.html(_.template($('#profile-tpl').html())); + + this.$nameField = $('#profile-name', this.$el); + this.$nameStatus = $('#profile-name-status', this.$el); + + this.$languageChoices = $('#preference-language', this.$el); + this.$languageStatus = $('#preference-language-status', this.$el); + + this.$submitStatus = $('#submit-status', this.$el); + + var self = this; + $.getJSON('preferences/languages') + .done(function(json) { + /** Asynchronously populate the language choices. */ + self.$languageChoices.html(_.template($('#languages-tpl').html(), {languageInfo: json})); + }) + .fail(function() { + self.$languageStatus + .addClass('language-list-error') + .text(gettext("We couldn't populate the list of language choices.")); + }); + + return this; + }, + + change: function() { + this.profileModel.set({ + fullName: this.$nameField.val() + }); + + this.preferencesModel.set({ + language: this.$languageChoices.val() + }); + }, + + submit: function(event) { + event.preventDefault(); + this.clearStatus(); + this.profileModel.save(); + this.preferencesModel.save(); + }, + + invalidProfile: function(model) { + var errors = model.validationError; + if (errors.hasOwnProperty('fullName')) { + this.$nameStatus + .addClass('validation-error') + .text(errors.fullName); + } + }, + + invalidPreference: function(model) { + var errors = model.validationError; + if (errors.hasOwnProperty('language')) { + this.$languageStatus + .addClass('validation-error') + .text(errors.language); + } + }, + + error: function(error) { + this.$submitStatus + .addClass('error') + .text(error); + }, + + sync: function() { + this.$submitStatus + .addClass('success') + .text(gettext("Saved")); + }, + + clearStatus: function() { + this.$nameStatus + .removeClass('validation-error') + .text(""); + + this.$languageStatus + .removeClass('validation-error') + .text(""); + + this.$submitStatus + .removeClass('error') + .text(""); + } + }); + + return new edx.student.profile.ProfileView({ + el: $('#profile-container') + }).render(); + +})(jQuery, _, Backbone, gettext); diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index dbc7f00e07..fa0bb93b48 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -72,6 +72,8 @@ spec_paths: fixture_paths: - templates/instructor/instructor_dashboard_2 - templates/dashboard + - templates/student_account + - templates/student_profile requirejs: paths: diff --git a/lms/templates/student_account/account.underscore b/lms/templates/student_account/account.underscore new file mode 100644 index 0000000000..9a6b91628f --- /dev/null +++ b/lms/templates/student_account/account.underscore @@ -0,0 +1,14 @@ +
+ + +
+ + + +
+ +
+ +
+
+ diff --git a/lms/templates/student_account/emails/email_change_request/message_body.txt b/lms/templates/student_account/emails/email_change_request/message_body.txt index 9c38042a5b..cc58ae814f 100644 --- a/lms/templates/student_account/emails/email_change_request/message_body.txt +++ b/lms/templates/student_account/emails/email_change_request/message_body.txt @@ -18,9 +18,9 @@ ${_("There was recently a request to change the email address associated " ## Confirmation link % if is_secure: -https://${site}/account/email_change_confirm/${key} +https://${site}/account/email/confirmation/${key} % else: -http://${site}/account/email_change_confirm/${key} +http://${site}/account/email/confirmation/${key} % endif ## Closing diff --git a/lms/templates/student_account/index.html b/lms/templates/student_account/index.html index a3403fb473..21ac4f4dda 100644 --- a/lms/templates/student_account/index.html +++ b/lms/templates/student_account/index.html @@ -6,23 +6,21 @@ <%block name="pagetitle">${_("Student Account")} <%block name="js_extra"> + + <%static:js group='student_account'/> +<%block name="header_extras"> +% for template_name in ["account"]: + +% endfor + +

Student Account

This is a placeholder for the student's account page.

-
- - - - - - - - -
- -
-
+
diff --git a/lms/templates/student_profile/index.html b/lms/templates/student_profile/index.html index a6d26ffdfe..aa46c69fcd 100644 --- a/lms/templates/student_profile/index.html +++ b/lms/templates/student_profile/index.html @@ -6,55 +6,24 @@ <%block name="pagetitle">${_("Student Profile")} <%block name="js_extra"> + + <%static:js group='student_profile'/> -

Student Profile

+<%block name="header_extras"> +% for template_name in ["profile", "languages"]: + +% endfor + + +

${_("Student Profile")}

This is a placeholder for the student's profile page.

-
- - - - - -
- -
-
- -
-
- - - - - -
- -
-
- - -
+
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): <%include file="third_party_auth.html" /> diff --git a/lms/templates/student_profile/languages.underscore b/lms/templates/student_profile/languages.underscore new file mode 100644 index 0000000000..5b11aff4f2 --- /dev/null +++ b/lms/templates/student_profile/languages.underscore @@ -0,0 +1,7 @@ +<% _.each( languageInfo.languages, function( language ){ %> + <% if ( language.name === languageInfo.preferredLanguage.name ){ %> + + <% } else { %> + + <% } %> +<% }); %> diff --git a/lms/templates/student_profile/profile.underscore b/lms/templates/student_profile/profile.underscore new file mode 100644 index 0000000000..76bafbc247 --- /dev/null +++ b/lms/templates/student_profile/profile.underscore @@ -0,0 +1,14 @@ +
+ + +
+ + + +
+ +
+ +
+
+