diff --git a/.gitignore b/.gitignore index 861ceec67f..e05cae25c6 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ codekit-config.json *.mo *.po *.prob +*.dup !django.po !django.mo !djangojs.po @@ -91,6 +92,9 @@ logs chromedriver.log ghostdriver.log +### Celery artifacts ### +celerybeat-schedule + ### Unknown artifacts database.sqlite courseware/static/js/mathjax/* diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 2ecb83f802..31afe010d5 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -19,6 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata from xmodule.fields import Date +from xmodule.tabs import InvalidTabsException from .utils import CourseTestCase from xmodule.modulestore.django import modulestore @@ -617,6 +618,7 @@ class CourseGradingTest(CourseTestCase): self.assertEqual(json.loads(response.content).get('graderType'), u'notgraded') +@ddt.ddt class CourseMetadataEditingTest(CourseTestCase): """ Tests for CourseMetadata. @@ -626,6 +628,7 @@ class CourseMetadataEditingTest(CourseTestCase): self.fullcourse = CourseFactory.create() self.course_setting_url = get_url(self.course.id, 'advanced_settings_handler') self.fullcourse_setting_url = get_url(self.fullcourse.id, 'advanced_settings_handler') + self.notes_tab = {"type": "notes", "name": "My Notes"} def test_fetch_initial_fields(self): test_model = CourseMetadata.fetch(self.course) @@ -930,12 +933,11 @@ class CourseMetadataEditingTest(CourseTestCase): """ open_ended_tab = {"type": "open_ended", "name": "Open Ended Panel"} peer_grading_tab = {"type": "peer_grading", "name": "Peer grading"} - notes_tab = {"type": "notes", "name": "My Notes"} # First ensure that none of the tabs are visible self.assertNotIn(open_ended_tab, self.course.tabs) self.assertNotIn(peer_grading_tab, self.course.tabs) - self.assertNotIn(notes_tab, self.course.tabs) + self.assertNotIn(self.notes_tab, self.course.tabs) # Now add the "combinedopenended" component and verify that the tab has been added self.client.ajax_post(self.course_setting_url, { @@ -944,7 +946,7 @@ class CourseMetadataEditingTest(CourseTestCase): course = modulestore().get_course(self.course.id) self.assertIn(open_ended_tab, course.tabs) self.assertIn(peer_grading_tab, course.tabs) - self.assertNotIn(notes_tab, course.tabs) + self.assertNotIn(self.notes_tab, course.tabs) # Now enable student notes and verify that the "My Notes" tab has also been added self.client.ajax_post(self.course_setting_url, { @@ -953,7 +955,7 @@ class CourseMetadataEditingTest(CourseTestCase): course = modulestore().get_course(self.course.id) self.assertIn(open_ended_tab, course.tabs) self.assertIn(peer_grading_tab, course.tabs) - self.assertIn(notes_tab, course.tabs) + self.assertIn(self.notes_tab, course.tabs) # Now remove the "combinedopenended" component and verify that the tab is gone self.client.ajax_post(self.course_setting_url, { @@ -962,7 +964,7 @@ class CourseMetadataEditingTest(CourseTestCase): course = modulestore().get_course(self.course.id) self.assertNotIn(open_ended_tab, course.tabs) self.assertNotIn(peer_grading_tab, course.tabs) - self.assertIn(notes_tab, course.tabs) + self.assertIn(self.notes_tab, course.tabs) # Finally disable student notes and verify that the "My Notes" tab is gone self.client.ajax_post(self.course_setting_url, { @@ -971,25 +973,40 @@ class CourseMetadataEditingTest(CourseTestCase): course = modulestore().get_course(self.course.id) self.assertNotIn(open_ended_tab, course.tabs) self.assertNotIn(peer_grading_tab, course.tabs) - self.assertNotIn(notes_tab, course.tabs) + self.assertNotIn(self.notes_tab, course.tabs) - def mark_wiki_as_hidden(self, tabs): - """ Mark the wiki tab as hidden. """ - for tab in tabs: - if tab.type == 'wiki': - tab['is_hidden'] = True - return tabs + def test_advanced_components_munge_tabs_validation_failure(self): + with patch('contentstore.views.course._refresh_course_tabs', side_effect=InvalidTabsException): + resp = self.client.ajax_post(self.course_setting_url, { + ADVANCED_COMPONENT_POLICY_KEY: {"value": ["notes"]} + }) + self.assertEqual(resp.status_code, 400) - def test_advanced_components_munge_tabs_hidden_tabs(self): - updated_tabs = self.mark_wiki_as_hidden(self.course.tabs) - self.course.tabs = updated_tabs + error_msg = [ + { + 'message': 'An error occurred while trying to save your tabs', + 'model': {'display_name': 'Tabs Exception'} + } + ] + self.assertEqual(json.loads(resp.content), error_msg) + + # verify that the course wasn't saved into the modulestore + course = modulestore().get_course(self.course.id) + self.assertNotIn("notes", course.advanced_modules) + + @ddt.data( + [{'type': 'courseware'}, {'type': 'course_info'}, {'type': 'wiki', 'is_hidden': True}], + [{'type': 'courseware', 'name': 'Courses'}, {'type': 'course_info', 'name': 'Info'}], + ) + def test_course_tab_configurations(self, tab_list): + self.course.tabs = tab_list modulestore().update_item(self.course, self.user.id) self.client.ajax_post(self.course_setting_url, { ADVANCED_COMPONENT_POLICY_KEY: {"value": ["notes"]} }) course = modulestore().get_course(self.course.id) - notes_tab = {"type": "notes", "name": "My Notes"} - self.assertIn(notes_tab, course.tabs) + tab_list.append(self.notes_tab) + self.assertEqual(tab_list, course.tabs) class CourseGraderUpdatesTest(CourseTestCase): diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index 691af12c39..c28be47d45 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -523,13 +523,13 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase): self.client.logout() self._assert_cannot_create_library(expected_code=302) # 302 redirect to login expected - # Now create a non-staff user with no permissions: + # Now check that logged-in users without CourseCreator role can still create libraries self._login_as_non_staff_user(logout_first=False) self.assertFalse(CourseCreatorRole().has_user(self.non_staff_user)) - - # Now check that logged-in users without any permissions cannot create libraries with patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}): - self._assert_cannot_create_library() + lib_key2 = self._create_library(library="lib2", display_name="Test Library 2") + library2 = modulestore().get_library(lib_key2) + self.assertIsNotNone(library2) @ddt.data( CourseInstructorRole, diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 8169f5499e..391a62d6b7 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -282,8 +282,8 @@ def get_component_templates(courselike, library=False): tab = 'common' if template['metadata'].get('markdown') is None: tab = 'advanced' - # Then the problem can override that with a tab: setting - tab = template['metadata'].get('tab', tab) + # Then the problem can override that with a tab: attribute (note: not nested in metadata) + tab = template.get('tab', tab) templates_for_category.append( create_template_dict( diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 10ef8f2167..d94be16f5b 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -5,6 +5,7 @@ import copy from django.shortcuts import redirect import json import random +import logging import string # pylint: disable=deprecated-module from django.utils.translation import ugettext as _ import django.utils @@ -22,7 +23,7 @@ from xmodule.course_module import DEFAULT_START_DATE from xmodule.error_module import ErrorDescriptor from xmodule.modulestore.django import modulestore from xmodule.contentstore.content import StaticContent -from xmodule.tabs import CourseTab +from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements @@ -87,6 +88,8 @@ from util.milestones_helpers import ( is_valid_course_key ) +log = logging.getLogger(__name__) + __all__ = ['course_info_handler', 'course_handler', 'course_listing', 'course_info_update_handler', 'course_search_index_handler', 'course_rerun_handler', @@ -455,6 +458,7 @@ def course_listing(request): 'in_process_course_actions': in_process_course_actions, 'libraries_enabled': LIBRARIES_ENABLED, 'libraries': [format_library_for_view(lib) for lib in libraries], + 'show_new_library_button': LIBRARIES_ENABLED and request.user.is_active, 'user': request.user, 'request_course_creator_url': reverse('contentstore.views.request_course_creator'), 'course_creator_status': _get_course_creator_status(request.user), @@ -1024,6 +1028,9 @@ def grading_handler(request, course_key_string, grader_index=None): def _refresh_course_tabs(request, course_module): """ Automatically adds/removes tabs if changes to the course require them. + + Raises: + InvalidTabsException: raised if there's a problem with the new version of the tabs. """ def update_tab(tabs, tab_type, tab_enabled): @@ -1047,6 +1054,8 @@ def _refresh_course_tabs(request, course_module): tab_enabled = tab_type.is_enabled(course_module, user=request.user) update_tab(course_tabs, tab_type, tab_enabled) + CourseTabList.validate_tabs(course_tabs) + # Save the tabs into the course if they have been changed if course_tabs != course_module.tabs: course_module.tabs = course_tabs @@ -1090,8 +1099,18 @@ def advanced_settings_handler(request, course_key_string): ) if is_valid: - # update the course tabs if required by any setting changes - _refresh_course_tabs(request, course_module) + try: + # update the course tabs if required by any setting changes + _refresh_course_tabs(request, course_module) + except InvalidTabsException as err: + log.exception(err.message) + response_message = [ + { + 'message': _('An error occurred while trying to save your tabs'), + 'model': {'display_name': _('Tabs Exception')} + } + ] + return JsonResponseBadRequest(response_message) # now update mongo modulestore().update_item(course_module, request.user.id) @@ -1101,7 +1120,7 @@ def advanced_settings_handler(request, course_key_string): return JsonResponseBadRequest(errors) # Handle all errors that validation doesn't catch - except (TypeError, ValueError) as err: + except (TypeError, ValueError, InvalidTabsException) as err: return HttpResponseBadRequest( django.utils.html.escape(err.message), content_type="text/plain" diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index 12987bfe0b..7d0fcf63ba 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -30,7 +30,7 @@ from .component import get_component_templates, CONTAINER_TEMPLATES from student.auth import ( STUDIO_VIEW_USERS, STUDIO_EDIT_ROLES, get_user_permissions, has_studio_read_access, has_studio_write_access ) -from student.roles import CourseCreatorRole, CourseInstructorRole, CourseStaffRole, LibraryUserRole +from student.roles import CourseInstructorRole, CourseStaffRole, LibraryUserRole from student import auth from util.json_request import expect_json, JsonResponse, JsonResponseBadRequest @@ -115,9 +115,6 @@ def _create_library(request): """ Helper method for creating a new library. """ - if not auth.has_access(request.user, CourseCreatorRole()): - log.exception(u"User %s tried to create a library without permission", request.user.username) - raise PermissionDenied() display_name = None try: display_name = request.json['display_name'] diff --git a/cms/djangoapps/contentstore/views/tests/test_library.py b/cms/djangoapps/contentstore/views/tests/test_library.py index a36ad45015..9e1abbbdf0 100644 --- a/cms/djangoapps/contentstore/views/tests/test_library.py +++ b/cms/djangoapps/contentstore/views/tests/test_library.py @@ -87,8 +87,8 @@ class UnitTestLibraries(ModuleStoreTestCase): @patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}) def test_lib_create_permission(self): """ - Users who aren't given course creator roles shouldn't be able to create - libraries either. + Users who are not given course creator roles should still be able to + create libraries. """ self.client.logout() ns_user, password = self.create_non_staff_user() @@ -97,7 +97,7 @@ class UnitTestLibraries(ModuleStoreTestCase): response = self.client.ajax_post(LIBRARY_REST_URL, { 'org': 'org', 'library': 'lib', 'display_name': "New Library", }) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 200) @ddt.data( {}, diff --git a/cms/envs/aws.py b/cms/envs/aws.py index b14c726e40..85396530fe 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -153,6 +153,12 @@ if ENV_TOKENS.get('SESSION_COOKIE_NAME', None): # NOTE, there's a bug in Django (http://bugs.python.org/issue18012) which necessitates this being a str() SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME')) +# Set the names of cookies shared with the marketing site +# These have the same cookie domain as the session, which in production +# usually includes subdomains. +EDXMKTG_LOGGED_IN_COOKIE_NAME = ENV_TOKENS.get('EDXMKTG_LOGGED_IN_COOKIE_NAME', EDXMKTG_LOGGED_IN_COOKIE_NAME) +EDXMKTG_USER_INFO_COOKIE_NAME = ENV_TOKENS.get('EDXMKTG_USER_INFO_COOKIE_NAME', EDXMKTG_USER_INFO_COOKIE_NAME) + #Email overrides DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL) DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL) diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index dcd120abf1..29f9accd97 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -1,5 +1,13 @@ """ -Settings for bok choy tests +Settings for Bok Choy tests that are used for running CMS and LMS. + +Bok Choy uses two different settings files: +1. test_static_optimized is used when invoking collectstatic +2. bok_choy is used when running CMS and LMS + +Note: it isn't possible to have a single settings file, because Django doesn't +support both generating static assets to a directory and also serving static +from the same directory. """ import os @@ -44,8 +52,20 @@ update_module_store_settings( default_store=os.environ.get('DEFAULT_STORE', 'draft'), ) -# Enable django-pipeline and staticfiles -STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath() +############################ STATIC FILES ############################# + +# Enable debug so that static assets are served by Django +DEBUG = True + +# Serve static files at /static directly from the staticfiles directory under test root +# Note: optimized files for testing are generated with settings from test_static_optimized +STATIC_URL = "/static/" +STATICFILES_FINDERS = ( + 'staticfiles.finders.FileSystemFinder', +) +STATICFILES_DIRS = ( + (TEST_ROOT / "staticfiles").abspath(), +) # Silence noisy logs import logging @@ -80,9 +100,6 @@ FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True -# Unfortunately, we need to use debug mode to serve staticfiles -DEBUG = True - # Point the URL used to test YouTube availability to our stub YouTube server YOUTUBE_PORT = 9080 YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) diff --git a/cms/envs/common.py b/cms/envs/common.py index eb8ec6e78c..f7485589cf 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -42,6 +42,9 @@ from lms.envs.common import ( # technically accessible through the CMS via legacy URLs. PROFILE_IMAGE_BACKEND, PROFILE_IMAGE_DEFAULT_FILENAME, PROFILE_IMAGE_DEFAULT_FILE_EXTENSION, PROFILE_IMAGE_SECRET_KEY, PROFILE_IMAGE_MIN_BYTES, PROFILE_IMAGE_MAX_BYTES, + # The following setting is included as it is used to check whether to + # display credit eligibility table on the CMS or not. + ENABLE_CREDIT_ELIGIBILITY ) from path import path from warnings import simplefilter @@ -174,7 +177,7 @@ FEATURES = { 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, # Enable credit eligibility feature - 'ENABLE_CREDIT_ELIGIBILITY': False, + 'ENABLE_CREDIT_ELIGIBILITY': ENABLE_CREDIT_ELIGIBILITY, # Can the visibility of the discussion tab be configured on a per-course basis? 'ALLOW_HIDING_DISCUSSION_TAB': False, @@ -248,7 +251,6 @@ from lms.envs.common import ( COURSE_KEY_PATTERN, COURSE_ID_PATTERN, USAGE_KEY_PATTERN, ASSET_KEY_PATTERN ) - ######################### CSRF ######################################### # Forwards-compatibility with Django 1.7 @@ -306,7 +308,9 @@ MIDDLEWARE_CLASSES = ( 'embargo.middleware.EmbargoMiddleware', # Detects user-requested locale from 'accept-language' header in http request - 'django.middleware.locale.LocaleMiddleware', + # TODO: Re-import the Django version once we upgrade to Django 1.8 [PLAT-671] + # 'django.middleware.locale.LocaleMiddleware', + 'django_locale.middleware.LocaleMiddleware', 'django.middleware.transaction.TransactionMiddleware', # needs to run after locale middleware (or anything that modifies the request context) @@ -750,6 +754,7 @@ INSTALLED_APPS = ( # Additional problem types 'edx_jsme', # Molecular Structure + 'openedx.core.djangoapps.content.course_overviews', 'openedx.core.djangoapps.content.course_structures', # Credit courses @@ -759,7 +764,10 @@ INSTALLED_APPS = ( ################# EDX MARKETING SITE ################################## -EDXMKTG_COOKIE_NAME = 'edxloggedin' +EDXMKTG_LOGGED_IN_COOKIE_NAME = 'edxloggedin' +EDXMKTG_USER_INFO_COOKIE_NAME = 'edx-user-info' +EDXMKTG_USER_INFO_COOKIE_VERSION = 1 + MKTG_URLS = {} MKTG_URL_LINK_MAP = { diff --git a/cms/envs/test.py b/cms/envs/test.py index 082960a804..f1351851cd 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -175,7 +175,7 @@ CACHES = { INSTALLED_APPS += ('external_auth', ) # Add milestones to Installed apps for testing -INSTALLED_APPS += ('milestones', ) +INSTALLED_APPS += ('milestones', 'openedx.core.djangoapps.call_stack_manager') # hide ratelimit warnings while running tests filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit') diff --git a/cms/envs/test_static_optimized.py b/cms/envs/test_static_optimized.py new file mode 100644 index 0000000000..c2b333b547 --- /dev/null +++ b/cms/envs/test_static_optimized.py @@ -0,0 +1,45 @@ +""" +Settings used when generating static assets for use in tests. + +For example, Bok Choy uses two different settings files: +1. test_static_optimized is used when invoking collectstatic +2. bok_choy is used when running CMS and LMS + +Note: it isn't possible to have a single settings file, because Django doesn't +support both generating static assets to a directory and also serving static +from the same directory. +""" + +import os +from path import path # pylint: disable=no-name-in-module + +# Pylint gets confused by path.py instances, which report themselves as class +# objects. As a result, pylint applies the wrong regex in validating names, +# and throws spurious errors. Therefore, we disable invalid-name checking. +# pylint: disable=invalid-name + + +########################## Prod-like settings ################################### +# These should be as close as possible to the settings we use in production. +# As in prod, we read in environment and auth variables from JSON files. +# Unlike in prod, we use the JSON files stored in this repo. +# This is a convenience for ensuring (a) that we can consistently find the files +# and (b) that the files are the same in Jenkins as in local dev. +os.environ['SERVICE_VARIANT'] = 'bok_choy' +os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() # pylint: disable=no-value-for-parameter + +from .aws import * # pylint: disable=wildcard-import, unused-wildcard-import + +######################### Testing overrides #################################### + +# Redirects to the test_root folder within the repo +TEST_ROOT = CONFIG_ROOT.dirname().dirname() / "test_root" # pylint: disable=no-value-for-parameter +LOG_DIR = (TEST_ROOT / "log").abspath() + +# Stores the static files under test root so that they don't overwrite existing static assets +STATIC_ROOT = (TEST_ROOT / "staticfiles").abspath() + +# Disables uglify when tests are running (used by build.js). +# 1. Uglify is by far the slowest part of the build process +# 2. Having full source code makes debugging tests easier for developers +os.environ['REQUIRE_BUILD_PROFILE_OPTIMIZE'] = 'none' diff --git a/cms/static/build.js b/cms/static/build.js index b01b4a5720..933041abe5 100644 --- a/cms/static/build.js +++ b/cms/static/build.js @@ -19,6 +19,10 @@ })); }; + + var jsOptimize = process.env.REQUIRE_BUILD_PROFILE_OPTIMIZE !== undefined ? + process.env.REQUIRE_BUILD_PROFILE_OPTIMIZE : 'uglify2'; + return { /** * List the modules that will be optimized. All their immediate and deep @@ -144,7 +148,7 @@ * mode to minify the code. Only available if REQUIRE_ENVIRONMENT is "rhino" (the default). * - "none": No minification will be done. */ - optimize: 'uglify2', + optimize: jsOptimize, /** * Sets the logging level. It is a number: * TRACE: 0, diff --git a/cms/static/js/index.js b/cms/static/js/index.js index e1c91d03a1..e4470ea22c 100644 --- a/cms/static/js/index.js +++ b/cms/static/js/index.js @@ -141,6 +141,8 @@ define(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape", "js/vie e.preventDefault(); $('.courses-tab').toggleClass('active', tab === 'courses'); $('.libraries-tab').toggleClass('active', tab === 'libraries'); + // Also toggle this course-related notice shown below the course tab, if it is present: + $('.wrapper-creationrights').toggleClass('is-hidden', tab === 'libraries'); }; }; diff --git a/cms/static/js/views/settings/grading.js b/cms/static/js/views/settings/grading.js index 0af6632c7f..def1ff1bc0 100644 --- a/cms/static/js/views/settings/grading.js +++ b/cms/static/js/views/settings/grading.js @@ -102,7 +102,7 @@ var GradingView = ValidatingView.extend({ renderMinimumGradeCredit: function() { var minimum_grade_credit = this.model.get('minimum_grade_credit'); this.$el.find('#course-minimum_grade_credit').val( - parseFloat(minimum_grade_credit) * 100 + '%' + Math.round(parseFloat(minimum_grade_credit) * 100) + '%' ); }, setGracePeriod : function(event) { diff --git a/cms/static/sass/elements/_footer.scss b/cms/static/sass/elements/_footer.scss index 2cc6eff83b..a95aed30ae 100644 --- a/cms/static/sass/elements/_footer.scss +++ b/cms/static/sass/elements/_footer.scss @@ -83,7 +83,6 @@ // platform Open edX logo and link .footer-about-openedx { @include float(right); - width: flex-grid(3,12); @include text-align(right); a { diff --git a/cms/templates/index.html b/cms/templates/index.html index ec93fe8b92..d8fe4389dd 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -24,13 +24,13 @@ % if course_creator_status=='granted': ${_("New Course")} - % if libraries_enabled: - - ${_("New Library")} - % endif % elif course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''): ${_("Email staff to create course")} % endif + % if show_new_library_button: + + ${_("New Library")} + % endif @@ -103,57 +103,57 @@ - %if libraries_enabled: -
-
-
- + % endif + + %if libraries_enabled and show_new_library_button: +
+ +
+ +
-
-

${_("Create a New Library")}

+
+

${_("Create a New Library")}

-
- ${_("Required Information to Create a New Library")} +
+ ${_("Required Information to Create a New Library")} -
    -
  1. - - ## Translators: This is an example name for a new content library, seen when filling out the form to create a new library. (A library is a collection of content or problems.) - - ${_("The public display name for your library.")} - -
  2. -
  3. - - - ${_("The public organization name for your library.")} ${_("This cannot be changed.")} - -
  4. +
      +
    1. + + ## Translators: This is an example name for a new content library, seen when filling out the form to create a new library. (A library is a collection of content or problems.) + + ${_("The public display name for your library.")} + +
    2. +
    3. + + + ${_("The public organization name for your library.")} ${_("This cannot be changed.")} + +
    4. -
    5. - - ## Translators: This is an example for the "code" used to identify a library, seen when filling out the form to create a new library. This example is short for "Computer Science Problems". The example number may contain letters but must not contain spaces. - - ${_("The unique code that identifies this library.")} ${_("Note: This is part of your library URL, so no spaces or special characters are allowed.")} ${_("This cannot be changed.")} - -
    6. -
    +
  5. + + ## Translators: This is an example for the "code" used to identify a library, seen when filling out the form to create a new library. This example is short for "Computer Science Problems". The example number may contain letters but must not contain spaces. + + ${_("The unique code that identifies this library.")} ${_("Note: This is part of your library URL, so no spaces or special characters are allowed.")} ${_("This cannot be changed.")} + +
  6. +
-
-
- -
- - - -
- -
- % endif + +
+
+ + + +
+ +
% endif @@ -449,7 +449,7 @@
- %if course_creator_status == "granted": + % if show_new_library_button:

${_('Create Your First Library')}

@@ -464,7 +464,7 @@
- %endif + % endif
%endif diff --git a/cms/templates/js/add-xblock-component-menu-problem.underscore b/cms/templates/js/add-xblock-component-menu-problem.underscore index d3324d1b62..301064935c 100644 --- a/cms/templates/js/add-xblock-component-menu-problem.underscore +++ b/cms/templates/js/add-xblock-component-menu-problem.underscore @@ -3,6 +3,9 @@
  • <%= gettext("Common Problem Types") %>
  • +
  • + <%= gettext("Common Problems with Hints and Feedback") %> +
  • <%= gettext("Advanced") %>
  • diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 31876cb599..d24b534cd7 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -130,8 +130,9 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
      % if 'grade' in credit_requirements:
    1. - + % for requirement in credit_requirements['grade']: + % endfor @@ -140,8 +141,9 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; % if 'proctored_exam' in credit_requirements:
    2. - + % for requirement in credit_requirements['proctored_exam']: + % endfor @@ -150,9 +152,10 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; % if 'reverification' in credit_requirements:
    3. - + % for requirement in credit_requirements['reverification']: ## Translators: 'Access to Assessment 1' means the access for a requirement with name 'Assessment 1' + % endfor diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index 701ad854c9..dd2281c791 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -85,8 +85,8 @@
      1. - - ${_("Must be greater than or equal to passing grade")} + + ${_("Must be greater than or equal to passing grade")}
      diff --git a/cms/templates/ux/reference/container.html b/cms/templates/ux/reference/container.html index 81821b503a..19ab1db780 100644 --- a/cms/templates/ux/reference/container.html +++ b/cms/templates/ux/reference/container.html @@ -351,7 +351,7 @@

      What Apple device competed with the portable CD player?

      -
      +
      diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index b09b4c6be2..290aef9249 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -119,7 +119,6 @@ class ChooseModeView(View): "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, - "can_audit": "audit" in modes, "responsive": True } if "verified" in modes: diff --git a/common/djangoapps/dark_lang/__init__.py b/common/djangoapps/dark_lang/__init__.py index d56fa38068..1e298276e0 100644 --- a/common/djangoapps/dark_lang/__init__.py +++ b/common/djangoapps/dark_lang/__init__.py @@ -17,3 +17,6 @@ Run migrations to install the configuration table. Use the admin site to add a new ``DarkLangConfig`` that is enabled, and lists the languages that should be released. """ + +# this is the UserPreference key for the currently-active dark language, if any +DARK_LANGUAGE_KEY = 'dark-lang' diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py index b18d064969..868ab05708 100644 --- a/common/djangoapps/dark_lang/middleware.py +++ b/common/djangoapps/dark_lang/middleware.py @@ -12,9 +12,17 @@ the SessionMiddleware. """ from django.conf import settings -from django.utils.translation.trans_real import parse_accept_lang_header - +from dark_lang import DARK_LANGUAGE_KEY from dark_lang.models import DarkLangConfig +from openedx.core.djangoapps.user_api.preferences.api import ( + delete_user_preference, get_user_preference, set_user_preference +) +from lang_pref import LANGUAGE_KEY + +# TODO re-import this once we're on Django 1.5 or greater. [PLAT-671] +# from django.utils.translation.trans_real import parse_accept_lang_header +# from django.utils.translation import LANGUAGE_SESSION_KEY +from django_locale.trans_real import parse_accept_lang_header, LANGUAGE_SESSION_KEY def dark_parse_accept_lang_header(accept): @@ -81,11 +89,17 @@ class DarkLangMiddleware(object): self._clean_accept_headers(request) self._activate_preview_language(request) - def _is_released(self, lang_code): - """ - ``True`` iff one of the values in ``self.released_langs`` is a prefix of ``lang_code``. - """ - return any(lang_code.lower().startswith(released_lang.lower()) for released_lang in self.released_langs) + def _fuzzy_match(self, lang_code): + """Returns a fuzzy match for lang_code""" + if lang_code in self.released_langs: + return lang_code + + lang_prefix = lang_code.partition('-')[0] + for released_lang in self.released_langs: + released_prefix = released_lang.partition('-')[0] + if lang_prefix == released_prefix: + return released_lang + return None def _format_accept_value(self, lang, priority=1.0): """ @@ -102,12 +116,13 @@ class DarkLangMiddleware(object): if accept is None or accept == '*': return - new_accept = ", ".join( - self._format_accept_value(lang, priority) - for lang, priority - in dark_parse_accept_lang_header(accept) - if self._is_released(lang) - ) + new_accept = [] + for lang, priority in dark_parse_accept_lang_header(accept): + fuzzy_code = self._fuzzy_match(lang.lower()) + if fuzzy_code: + new_accept.append(self._format_accept_value(fuzzy_code, priority)) + + new_accept = ", ".join(new_accept) request.META['HTTP_ACCEPT_LANGUAGE'] = new_accept @@ -115,15 +130,38 @@ class DarkLangMiddleware(object): """ If the request has the get parameter ``preview-lang``, and that language doesn't appear in ``self.released_langs``, - then set the session ``django_language`` to that language. + then set the session LANGUAGE_SESSION_KEY to that language. """ + auth_user = request.user.is_authenticated() if 'clear-lang' in request.GET: - if 'django_language' in request.session: - del request.session['django_language'] + # delete the session language key (if one is set) + if LANGUAGE_SESSION_KEY in request.session: + del request.session[LANGUAGE_SESSION_KEY] + if auth_user: + # Reset user's dark lang preference to null + delete_user_preference(request.user, DARK_LANGUAGE_KEY) + # Get & set user's preferred language + user_pref = get_user_preference(request.user, LANGUAGE_KEY) + if user_pref: + request.session[LANGUAGE_SESSION_KEY] = user_pref + return + + # Get the user's preview lang - this is either going to be set from a query + # param `?preview-lang=xx`, or we may have one already set as a dark lang preference. preview_lang = request.GET.get('preview-lang', None) + if not preview_lang and auth_user: + # Get the request user's dark lang preference + preview_lang = get_user_preference(request.user, DARK_LANGUAGE_KEY) + # User doesn't have a dark lang preference, so just return if not preview_lang: return - request.session['django_language'] = preview_lang + # Set the session key to the requested preview lang + request.session[LANGUAGE_SESSION_KEY] = preview_lang + + # Make sure that we set the requested preview lang as the dark lang preference for the + # user, so that the lang_pref middleware doesn't clobber away the dark lang preview. + if auth_user: + set_user_preference(request.user, DARK_LANGUAGE_KEY, preview_lang) diff --git a/common/djangoapps/dark_lang/models.py b/common/djangoapps/dark_lang/models.py index 1daec994e8..e61ea41fb2 100644 --- a/common/djangoapps/dark_lang/models.py +++ b/common/djangoapps/dark_lang/models.py @@ -25,7 +25,7 @@ class DarkLangConfig(ConfigurationModel): if not self.released_languages.strip(): # pylint: disable=no-member return [] - languages = [lang.strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member + languages = [lang.lower().strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member # Put in alphabetical order languages.sort() return languages diff --git a/common/djangoapps/dark_lang/tests.py b/common/djangoapps/dark_lang/tests.py index 6dd0b41882..13519b32eb 100644 --- a/common/djangoapps/dark_lang/tests.py +++ b/common/djangoapps/dark_lang/tests.py @@ -4,11 +4,17 @@ Tests of DarkLangMiddleware from django.contrib.auth.models import User from django.http import HttpRequest +import ddt from django.test import TestCase from mock import Mock +import unittest from dark_lang.middleware import DarkLangMiddleware from dark_lang.models import DarkLangConfig +# TODO PLAT-671 Import from Django 1.8 +# from django.utils.translation import LANGUAGE_SESSION_KEY +from django_locale.trans_real import LANGUAGE_SESSION_KEY +from student.tests.factories import UserFactory UNSET = object() @@ -23,6 +29,7 @@ def set_if_set(dct, key, value): dct[key] = value +@ddt.ddt class DarkLangMiddlewareTests(TestCase): """ Tests of DarkLangMiddleware @@ -37,18 +44,18 @@ class DarkLangMiddlewareTests(TestCase): enabled=True ).save() - def process_request(self, django_language=UNSET, accept=UNSET, preview_lang=UNSET, clear_lang=UNSET): + def process_request(self, language_session_key=UNSET, accept=UNSET, preview_lang=UNSET, clear_lang=UNSET): """ Build a request and then process it using the ``DarkLangMiddleware``. Args: - django_language (str): The language code to set in request.session['django_language'] + language_session_key (str): The language code to set in request.session[LANUGAGE_SESSION_KEY] accept (str): The accept header to set in request.META['HTTP_ACCEPT_LANGUAGE'] preview_lang (str): The value to set in request.GET['preview_lang'] clear_lang (str): The value to set in request.GET['clear_lang'] """ session = {} - set_if_set(session, 'django_language', django_language) + set_if_set(session, LANGUAGE_SESSION_KEY, language_session_key) meta = {} set_if_set(meta, 'HTTP_ACCEPT_LANGUAGE', accept) @@ -61,7 +68,8 @@ class DarkLangMiddlewareTests(TestCase): spec=HttpRequest, session=session, META=meta, - GET=get + GET=get, + user=UserFactory() ) self.assertIsNone(DarkLangMiddleware().process_request(request)) return request @@ -82,6 +90,10 @@ class DarkLangMiddlewareTests(TestCase): def test_wildcard_accept(self): self.assertAcceptEquals('*', self.process_request(accept='*')) + def test_malformed_accept(self): + self.assertAcceptEquals('', self.process_request(accept='xxxxxxxxxxxx')) + self.assertAcceptEquals('', self.process_request(accept='en;q=1.0, es-419:q-0.8')) + def test_released_accept(self): self.assertAcceptEquals( 'rel;q=1.0', @@ -123,14 +135,17 @@ class DarkLangMiddlewareTests(TestCase): ) def test_accept_released_territory(self): + # We will munge 'rel-ter' to be 'rel', so the 'rel-ter' + # user will actually receive the released language 'rel' + # (Otherwise, the user will actually end up getting the server default) self.assertAcceptEquals( - 'rel-ter;q=1.0, rel;q=0.5', + 'rel;q=1.0, rel;q=0.5', self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') ) def test_accept_mixed_case(self): self.assertAcceptEquals( - 'rel-TER;q=1.0, REL;q=0.5', + 'rel;q=1.0, rel;q=0.5', self.process_request(accept='rel-TER;q=1.0, REL;q=0.5') ) @@ -140,18 +155,92 @@ class DarkLangMiddlewareTests(TestCase): enabled=True ).save() + # Since we have only released "rel-ter", the requested code "rel" will + # fuzzy match to "rel-ter", in addition to "rel-ter" exact matching "rel-ter" self.assertAcceptEquals( - 'rel-ter;q=1.0', + 'rel-ter;q=1.0, rel-ter;q=0.5', self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') ) + @ddt.data( + ('es;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es' should get 'es-419', not English + ('es-AR;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es-AR' should get 'es-419', not English + ) + @ddt.unpack + def test_partial_match_es419(self, accept_header, expected): + # Release es-419 + DarkLangConfig( + released_languages=('es-419, en'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + expected, + self.process_request(accept=accept_header) + ) + + def test_partial_match_esar_es(self): + # If I release 'es', 'es-AR' should get 'es', not English + DarkLangConfig( + released_languages=('es, en'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + 'es;q=1.0', + self.process_request(accept='es-AR;q=1.0, pt;q=0.5') + ) + + @ddt.data( + # Test condition: If I release 'es-419, es, es-es'... + ('es;q=1.0, pt;q=0.5', 'es;q=1.0'), # 1. es should get es + ('es-419;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 2. es-419 should get es-419 + ('es-es;q=1.0, pt;q=0.5', 'es-es;q=1.0'), # 3. es-es should get es-es + ) + @ddt.unpack + def test_exact_match_gets_priority(self, accept_header, expected): + # Release 'es-419, es, es-es' + DarkLangConfig( + released_languages=('es-419, es, es-es'), + changed_by=self.user, + enabled=True + ).save() + self.assertAcceptEquals( + expected, + self.process_request(accept=accept_header) + ) + + @unittest.skip("This won't work until fallback is implemented for LA country codes. See LOC-86") + @ddt.data( + 'es-AR', # Argentina + 'es-PY', # Paraguay + ) + def test_partial_match_es_la(self, latin_america_code): + # We need to figure out the best way to implement this. There are a ton of LA country + # codes that ought to fall back to 'es-419' rather than 'es-es'. + # http://unstats.un.org/unsd/methods/m49/m49regin.htm#americas + # If I release 'es, es-419' + # Latin American codes should get es-419 + DarkLangConfig( + released_languages=('es, es-419'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + 'es-419;q=1.0', + self.process_request(accept='{};q=1.0, pt;q=0.5'.format(latin_america_code)) + ) + def assertSessionLangEquals(self, value, request): """ - Assert that the 'django_language' set in request.session is equal to value + Assert that the LANGUAGE_SESSION_KEY set in request.session is equal to value """ self.assertEquals( value, - request.session.get('django_language', UNSET) + request.session.get(LANGUAGE_SESSION_KEY, UNSET) ) def test_preview_lang_with_released_language(self): @@ -163,7 +252,7 @@ class DarkLangMiddlewareTests(TestCase): self.assertSessionLangEquals( 'rel', - self.process_request(preview_lang='rel', django_language='notrel') + self.process_request(preview_lang='rel', language_session_key='notrel') ) def test_preview_lang_with_dark_language(self): @@ -174,7 +263,7 @@ class DarkLangMiddlewareTests(TestCase): self.assertSessionLangEquals( 'unrel', - self.process_request(preview_lang='unrel', django_language='notrel') + self.process_request(preview_lang='unrel', language_session_key='notrel') ) def test_clear_lang(self): @@ -185,12 +274,12 @@ class DarkLangMiddlewareTests(TestCase): self.assertSessionLangEquals( UNSET, - self.process_request(clear_lang=True, django_language='rel') + self.process_request(clear_lang=True, language_session_key='rel') ) self.assertSessionLangEquals( UNSET, - self.process_request(clear_lang=True, django_language='unrel') + self.process_request(clear_lang=True, language_session_key='unrel') ) def test_disabled(self): @@ -203,17 +292,17 @@ class DarkLangMiddlewareTests(TestCase): self.assertSessionLangEquals( 'rel', - self.process_request(clear_lang=True, django_language='rel') + self.process_request(clear_lang=True, language_session_key='rel') ) self.assertSessionLangEquals( 'unrel', - self.process_request(clear_lang=True, django_language='unrel') + self.process_request(clear_lang=True, language_session_key='unrel') ) self.assertSessionLangEquals( 'rel', - self.process_request(preview_lang='unrel', django_language='rel') + self.process_request(preview_lang='unrel', language_session_key='rel') ) def test_accept_chinese_language_codes(self): @@ -224,6 +313,6 @@ class DarkLangMiddlewareTests(TestCase): ).save() self.assertAcceptEquals( - 'zh-CN;q=1.0, zh-TW;q=0.5, zh-HK;q=0.3', + 'zh-cn;q=1.0, zh-tw;q=0.5, zh-hk;q=0.3', self.process_request(accept='zh-Hans;q=1.0, zh-Hant-TW;q=0.5, zh-HK;q=0.3') ) diff --git a/common/djangoapps/django_locale/__init__.py b/common/djangoapps/django_locale/__init__.py new file mode 100644 index 0000000000..655019022e --- /dev/null +++ b/common/djangoapps/django_locale/__init__.py @@ -0,0 +1,7 @@ +""" +TODO: This module is imported from the stable Django 1.8 branch, as a +copy of https://github.com/django/django/blob/stable/1.8.x/django/middleware/locale.py. + +Remove this file and re-import this middleware from Django once the +codebase is upgraded with a modern version of Django. [PLAT-671] +""" diff --git a/common/djangoapps/django_locale/middleware.py b/common/djangoapps/django_locale/middleware.py new file mode 100644 index 0000000000..b0601a807e --- /dev/null +++ b/common/djangoapps/django_locale/middleware.py @@ -0,0 +1,83 @@ +# TODO: This file is imported from the stable Django 1.8 branch. Remove this file +# and re-import this middleware from Django once the codebase is upgraded. [PLAT-671] +# pylint: disable=invalid-name, missing-docstring +"This is the locale selecting middleware that will look at accept headers" + +from django.conf import settings +from django.core.urlresolvers import ( + LocaleRegexURLResolver, get_resolver, get_script_prefix, is_valid_path, +) +from django.http import HttpResponseRedirect +from django.utils import translation +from django.utils.cache import patch_vary_headers +# Override the Django 1.4 implementation with the 1.8 implementation +from django_locale.trans_real import get_language_from_request + + +class LocaleMiddleware(object): + """ + This is a very simple middleware that parses a request + and decides what translation object to install in the current + thread context. This allows pages to be dynamically + translated to the language the user desires (if the language + is available, of course). + """ + response_redirect_class = HttpResponseRedirect + + def __init__(self): + self._is_language_prefix_patterns_used = False + for url_pattern in get_resolver(None).url_patterns: + if isinstance(url_pattern, LocaleRegexURLResolver): + self._is_language_prefix_patterns_used = True + break + + def process_request(self, request): + check_path = self.is_language_prefix_patterns_used() + # This call is broken in Django 1.4: + # https://github.com/django/django/blob/stable/1.4.x/django/utils/translation/trans_real.py#L399 + # (we override parse_accept_lang_header to a fixed version in dark_lang.middleware) + language = get_language_from_request( + request, check_path=check_path) + translation.activate(language) + request.LANGUAGE_CODE = translation.get_language() + + def process_response(self, request, response): + language = translation.get_language() + language_from_path = translation.get_language_from_path(request.path_info) + if (response.status_code == 404 and not language_from_path + and self.is_language_prefix_patterns_used()): + urlconf = getattr(request, 'urlconf', None) + language_path = '/%s%s' % (language, request.path_info) + path_valid = is_valid_path(language_path, urlconf) + if (not path_valid and settings.APPEND_SLASH + and not language_path.endswith('/')): + path_valid = is_valid_path("%s/" % language_path, urlconf) + + if path_valid: + script_prefix = get_script_prefix() + language_url = "%s://%s%s" % ( + request.scheme, + request.get_host(), + # insert language after the script prefix and before the + # rest of the URL + request.get_full_path().replace( + script_prefix, + '%s%s/' % (script_prefix, language), + 1 + ) + ) + return self.response_redirect_class(language_url) + + if not (self.is_language_prefix_patterns_used() + and language_from_path): + patch_vary_headers(response, ('Accept-Language',)) + if 'Content-Language' not in response: + response['Content-Language'] = language + return response + + def is_language_prefix_patterns_used(self): + """ + Returns `True` if the `LocaleRegexURLResolver` is used + at root level of the urlpatterns, else it returns `False`. + """ + return self._is_language_prefix_patterns_used diff --git a/common/djangoapps/django_locale/tests.py b/common/djangoapps/django_locale/tests.py new file mode 100644 index 0000000000..cc40ce9d4a --- /dev/null +++ b/common/djangoapps/django_locale/tests.py @@ -0,0 +1,157 @@ +# pylint: disable=invalid-name, line-too-long, super-method-not-called +""" +Tests taken from Django upstream: +https://github.com/django/django/blob/e6b34193c5c7d117ededdab04bb16caf8864f07c/tests/regressiontests/i18n/tests.py +""" +from django.conf import settings +from django.test import TestCase, RequestFactory +from django_locale.trans_real import ( + parse_accept_lang_header, get_language_from_request, LANGUAGE_SESSION_KEY +) + +# Added to test middleware around dark lang +from django.contrib.auth.models import User +from django.test.utils import override_settings +from dark_lang.models import DarkLangConfig + + +# Adding to support test differences between Django and our own settings +@override_settings(LANGUAGES=[ + ('pt', 'Portuguese'), + ('pt-br', 'Portuguese-Brasil'), + ('es', 'Spanish'), + ('es-ar', 'Spanish (Argentina)'), + ('de', 'Deutch'), + ('zh-cn', 'Chinese (China)'), + ('ar-sa', 'Arabic (Saudi Arabia)'), +]) +class MiscTests(TestCase): + """ + Tests taken from Django upstream: + https://github.com/django/django/blob/e6b34193c5c7d117ededdab04bb16caf8864f07c/tests/regressiontests/i18n/tests.py + """ + def setUp(self): + self.rf = RequestFactory() + # Added to test middleware around dark lang + user = User() + user.save() + DarkLangConfig( + released_languages='pt, pt-br, es, de, es-ar, zh-cn, ar-sa', + changed_by=user, + enabled=True + ).save() + + def test_parse_spec_http_header(self): + """ + Testing HTTP header parsing. First, we test that we can parse the + values according to the spec (and that we extract all the pieces in + the right order). + """ + p = parse_accept_lang_header + # Good headers. + self.assertEqual([('de', 1.0)], p('de')) + self.assertEqual([('en-AU', 1.0)], p('en-AU')) + self.assertEqual([('es-419', 1.0)], p('es-419')) + self.assertEqual([('*', 1.0)], p('*;q=1.00')) + self.assertEqual([('en-AU', 0.123)], p('en-AU;q=0.123')) + self.assertEqual([('en-au', 0.5)], p('en-au;q=0.5')) + self.assertEqual([('en-au', 1.0)], p('en-au;q=1.0')) + self.assertEqual([('da', 1.0), ('en', 0.5), ('en-gb', 0.25)], p('da, en-gb;q=0.25, en;q=0.5')) + self.assertEqual([('en-au-xx', 1.0)], p('en-au-xx')) + self.assertEqual([('de', 1.0), ('en-au', 0.75), ('en-us', 0.5), ('en', 0.25), ('es', 0.125), ('fa', 0.125)], p('de,en-au;q=0.75,en-us;q=0.5,en;q=0.25,es;q=0.125,fa;q=0.125')) + self.assertEqual([('*', 1.0)], p('*')) + self.assertEqual([('de', 1.0)], p('de;q=0.')) + self.assertEqual([('en', 1.0), ('*', 0.5)], p('en; q=1.0, * ; q=0.5')) + self.assertEqual([], p('')) + + # Bad headers; should always return []. + self.assertEqual([], p('en-gb;q=1.0000')) + self.assertEqual([], p('en;q=0.1234')) + self.assertEqual([], p('en;q=.2')) + self.assertEqual([], p('abcdefghi-au')) + self.assertEqual([], p('**')) + self.assertEqual([], p('en,,gb')) + self.assertEqual([], p('en-au;q=0.1.0')) + self.assertEqual([], p('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ,en')) + self.assertEqual([], p('da, en-gb;q=0.8, en;q=0.7,#')) + self.assertEqual([], p('de;q=2.0')) + self.assertEqual([], p('de;q=0.a')) + self.assertEqual([], p('12-345')) + self.assertEqual([], p('')) + + def test_parse_literal_http_header(self): + """ + Now test that we parse a literal HTTP header correctly. + """ + g = get_language_from_request + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'} + self.assertEqual('pt-br', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt'} + self.assertEqual('pt', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es,de'} + self.assertEqual('es', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-ar,de'} + self.assertEqual('es-ar', g(r)) + + # This test assumes there won't be a Django translation to a US + # variation of the Spanish language, a safe assumption. When the + # user sets it as the preferred language, the main 'es' + # translation should be selected instead. + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-us'} + self.assertEqual(g(r), 'es') + + # This tests the following scenario: there isn't a main language (zh) + # translation of Django but there is a translation to variation (zh_CN) + # the user sets zh-cn as the preferred language, it should be selected + # by Django without falling back nor ignoring it. + r.META = {'HTTP_ACCEPT_LANGUAGE': 'zh-cn,de'} + self.assertEqual(g(r), 'zh-cn') + + def test_logic_masked_by_darklang(self): + g = get_language_from_request + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'ar-qa'} + self.assertEqual('ar-sa', g(r)) + + r.session = {LANGUAGE_SESSION_KEY: 'es'} + self.assertEqual('es', g(r)) + + def test_parse_language_cookie(self): + """ + Now test that we parse language preferences stored in a cookie correctly. + """ + g = get_language_from_request + r = self.rf.get('/') + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'} + r.META = {} + self.assertEqual('pt-br', g(r)) + + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt'} + r.META = {} + self.assertEqual('pt', g(r)) + + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es'} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} + self.assertEqual('es', g(r)) + + # This test assumes there won't be a Django translation to a US + # variation of the Spanish language, a safe assumption. When the + # user sets it as the preferred language, the main 'es' + # translation should be selected instead. + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es-us'} + r.META = {} + self.assertEqual(g(r), 'es') + + # This tests the following scenario: there isn't a main language (zh) + # translation of Django but there is a translation to variation (zh_CN) + # the user sets zh-cn as the preferred language, it should be selected + # by Django without falling back nor ignoring it. + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-cn'} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} + self.assertEqual(g(r), 'zh-cn') diff --git a/common/djangoapps/django_locale/trans_real.py b/common/djangoapps/django_locale/trans_real.py new file mode 100644 index 0000000000..3ec6b6d026 --- /dev/null +++ b/common/djangoapps/django_locale/trans_real.py @@ -0,0 +1,131 @@ +"""Translation helper functions.""" +# Imported from Django 1.8 +# pylint: disable=invalid-name +import re +from django.conf import settings +from django.conf.locale import LANG_INFO +from django.utils import translation + + +# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9. +# and RFC 3066, section 2.1 +accept_language_re = re.compile(r''' + ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z", "*" + (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8" + (?:\s*,\s*|$) # Multiple accepts per header. + ''', re.VERBOSE) + + +language_code_re = re.compile(r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*$', re.IGNORECASE) + + +LANGUAGE_SESSION_KEY = '_language' + + +def parse_accept_lang_header(lang_string): + """ + Parses the lang_string, which is the body of an HTTP Accept-Language + header, and returns a list of (lang, q-value), ordered by 'q' values. + + Any format errors in lang_string results in an empty list being returned. + """ + # parse_accept_lang_header is broken until we are on Django 1.5 or greater + # See https://code.djangoproject.com/ticket/19381 + result = [] + pieces = accept_language_re.split(lang_string) + if pieces[-1]: + return [] + for i in range(0, len(pieces) - 1, 3): + first, lang, priority = pieces[i: i + 3] + if first: + return [] + priority = priority and float(priority) or 1.0 + result.append((lang, priority)) + result.sort(key=lambda k: k[1], reverse=True) + return result + + +def get_supported_language_variant(lang_code, strict=False): + """ + Returns the language-code that's listed in supported languages, possibly + selecting a more generic variant. Raises LookupError if nothing found. + If `strict` is False (the default), the function will look for an alternative + country-specific variant when the currently checked is not found. + lru_cache should have a maxsize to prevent from memory exhaustion attacks, + as the provided language codes are taken from the HTTP request. See also + . + """ + if lang_code: + # If 'fr-ca' is not supported, try special fallback or language-only 'fr'. + possible_lang_codes = [lang_code] + try: + # TODO skip this, or import updated LANG_INFO format from __future__ + # (fallback option wasn't added until + # https://github.com/django/django/commit/5dcdbe95c749d36072f527e120a8cb463199ae0d) + possible_lang_codes.extend(LANG_INFO[lang_code]['fallback']) + except KeyError: + pass + generic_lang_code = lang_code.split('-')[0] + possible_lang_codes.append(generic_lang_code) + supported_lang_codes = dict(settings.LANGUAGES) + + for code in possible_lang_codes: + # Note: django 1.4 implementation of check_for_language is OK to use + if code in supported_lang_codes and translation.check_for_language(code): + return code + if not strict: + # if fr-fr is not supported, try fr-ca. + for supported_code in supported_lang_codes: + if supported_code.startswith(generic_lang_code + '-'): + return supported_code + raise LookupError(lang_code) + + +def get_language_from_request(request, check_path=False): + """ + Analyzes the request to find what language the user wants the system to + show. Only languages listed in settings.LANGUAGES are taken into account. + If the user requests a sublanguage where we have a main language, we send + out the main language. + If check_path is True, the URL path prefix will be checked for a language + code, otherwise this is skipped for backwards compatibility. + """ + if check_path: + # Note: django 1.4 implementation of get_language_from_path is OK to use + lang_code = translation.get_language_from_path(request.path_info) + if lang_code is not None: + return lang_code + + supported_lang_codes = dict(settings.LANGUAGES) + + if hasattr(request, 'session'): + lang_code = request.session.get(LANGUAGE_SESSION_KEY) + # Note: django 1.4 implementation of check_for_language is OK to use + if lang_code in supported_lang_codes and lang_code is not None and translation.check_for_language(lang_code): + return lang_code + + lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) + + try: + return get_supported_language_variant(lang_code) + except LookupError: + pass + + accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') + # broken in 1.4, so defined above + for accept_lang, unused in parse_accept_lang_header(accept): + if accept_lang == '*': + break + + if not language_code_re.search(accept_lang): + continue + + try: + return get_supported_language_variant(accept_lang) + except LookupError: + continue + + try: + return get_supported_language_variant(settings.LANGUAGE_CODE) + except LookupError: + return settings.LANGUAGE_CODE diff --git a/common/djangoapps/embargo/api.py b/common/djangoapps/embargo/api.py index 1142db9116..c928dd7946 100644 --- a/common/djangoapps/embargo/api.py +++ b/common/djangoapps/embargo/api.py @@ -10,6 +10,9 @@ import pygeoip from django.core.cache import cache from django.conf import settings +from rest_framework.response import Response +from rest_framework import status +from ipware.ip import get_ip from embargo.models import CountryAccessRule, RestrictedCourse @@ -166,3 +169,30 @@ def _country_code_from_ip(ip_addr): return pygeoip.GeoIP(settings.GEOIPV6_PATH).country_code_by_addr(ip_addr) else: return pygeoip.GeoIP(settings.GEOIP_PATH).country_code_by_addr(ip_addr) + + +def get_embargo_response(request, course_id, user): + """ + Check whether any country access rules block the user from enrollment. + + Args: + request (HttpRequest): The request object + course_id (str): The requested course ID + user (str): The current user object + + Returns: + HttpResponse: Response of the embargo page if embargoed, None if not + + """ + redirect_url = redirect_if_blocked( + course_id, user=user, ip_address=get_ip(request), url=request.path) + if redirect_url: + return Response( + status=status.HTTP_403_FORBIDDEN, + data={ + "message": ( + u"Users from this location cannot access the course '{course_id}'." + ).format(course_id=course_id), + "user_message_url": request.build_absolute_uri(redirect_url) + } + ) diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index 5d4355fd04..7fa5cf9e10 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -26,6 +26,7 @@ from util.models import RateLimitConfiguration from util.testing import UrlResetMixin from enrollment import api from enrollment.errors import CourseEnrollmentError +from openedx.core.lib.django_test_client_utils import get_absolute_url from openedx.core.djangoapps.user_api.models import UserOrgTag from student.tests.factories import UserFactory, CourseModeFactory from student.models import CourseEnrollment @@ -725,10 +726,6 @@ class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestC 'user': self.user.username }) - def _get_absolute_url(self, path): - """ Generate an absolute URL for a resource on the test server. """ - return u'http://testserver/{}'.format(path.lstrip('/')) - def assert_access_denied(self, user_message_path): """ Verify that the view returns HTTP status 403 and includes a URL in the response, and no enrollment is created. @@ -741,7 +738,7 @@ class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestC # Expect that the redirect URL is included in the response resp_data = json.loads(response.content) - user_message_url = self._get_absolute_url(user_message_path) + user_message_url = get_absolute_url(user_message_path) self.assertEqual(resp_data['user_message_url'], user_message_url) # Verify that we were not enrolled diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index eef151a8b9..a8943d7c3b 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -32,7 +32,6 @@ from enrollment.errors import ( ) from student.models import User - log = logging.getLogger(__name__) @@ -91,15 +90,19 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn): * course_id: The unique identifier for the course. - * enrollment_start: The date and time that users can begin enrolling in the course. If null, enrollment opens immediately when the course is created. + * enrollment_start: The date and time that users can begin enrolling in the course. + If null, enrollment opens immediately when the course is created. - * enrollment_end: The date and time after which users cannot enroll for the course. If null, the enrollment period never ends. + * enrollment_end: The date and time after which users cannot enroll for the course. + If null, the enrollment period never ends. - * course_start: The date and time at which the course opens. If null, the course opens immediately when created. + * course_start: The date and time at which the course opens. + If null, the course opens immediately when created. * course_end: The date and time at which the course closes. If null, the course never ends. - * course_modes: An array of data about the enrollment modes supported for the course. Each enrollment mode collection includes: + * course_modes: An array of data about the enrollment modes supported for the course. + Each enrollment mode collection includes: * slug: The short name for the enrollment mode. * name: The full name of the enrollment mode. @@ -182,20 +185,24 @@ class EnrollmentCourseDetailView(APIView): **Response Values** - A collection of course enrollments for the user, or for the newly created enrollment. Each course enrollment contains: + A collection of course enrollments for the user, or for the newly created enrollment. + Each course enrollment contains: * course_id: The unique identifier of the course. - * enrollment_start: The date and time that users can begin enrolling in the course. If null, enrollment opens immediately when the course is created. + * enrollment_start: The date and time that users can begin enrolling in the course. + If null, enrollment opens immediately when the course is created. - * enrollment_end: The date and time after which users cannot enroll for the course. If null, the enrollment period never ends. + * enrollment_end: The date and time after which users cannot enroll for the course. + If null, the enrollment period never ends. - * course_start: The date and time at which the course opens. If null, the course opens immediately when created. + * course_start: The date and time at which the course opens. + If null, the course opens immediately when created. * course_end: The date and time at which the course closes. If null, the course never ends. * course_modes: An array containing details about the enrollment modes supported for the course. - If the request uses the parameter include_expired=1, the array also includes expired enrollment modes. + If the request uses the parameter include_expired=1, the array also includes expired enrollment modes. Each enrollment mode collection includes: @@ -204,7 +211,8 @@ class EnrollmentCourseDetailView(APIView): * min_price: The minimum price for which a user can enroll in this mode. * suggested_prices: A list of suggested prices for this enrollment mode. * currency: The currency of the listed prices. - * expiration_datetime: The date and time after which users cannot enroll in the course in this mode. + * expiration_datetime: The date and time after which users cannot enroll in the course + in this mode. * description: A description of this mode. * invite_only: Whether students must be invited to enroll in the course; true or false. @@ -267,7 +275,8 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): **Post Parameters** - * user: The user ID of the currently logged in user. Optional. You cannot use the command to enroll a different user. + * user: The username of the currently logged in user. Optional. + You cannot use the command to enroll a different user. * mode: The Course Mode for the enrollment. Individual users cannot upgrade their enrollment mode from 'honor'. Only server-to-server requests can enroll with other modes. Optional. @@ -284,7 +293,8 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): **Response Values** - A collection of course enrollments for the user, or for the newly created enrollment. Each course enrollment contains: + A collection of course enrollments for the user, or for the newly created enrollment. + Each course enrollment contains: * created: The date the user account was created. @@ -296,28 +306,33 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): * course_id: The unique identifier for the course. - * enrollment_start: The date and time that users can begin enrolling in the course. If null, enrollment opens immediately when the course is created. + * enrollment_start: The date and time that users can begin enrolling in the course. + If null, enrollment opens immediately when the course is created. - * enrollment_end: The date and time after which users cannot enroll for the course. If null, the enrollment period never ends. + * enrollment_end: The date and time after which users cannot enroll for the course. + If null, the enrollment period never ends. - * course_start: The date and time at which the course opens. If null, the course opens immediately when created. + * course_start: The date and time at which the course opens. + If null, the course opens immediately when created. * course_end: The date and time at which the course closes. If null, the course never ends. - * course_modes: An array of data about the enrollment modes supported for the course. Each enrollment mode collection includes: + * course_modes: An array of data about the enrollment modes supported for the course. + Each enrollment mode collection includes: * slug: The short name for the enrollment mode. * name: The full name of the enrollment mode. * min_price: The minimum price for which a user can enroll in this mode. * suggested_prices: A list of suggested prices for this enrollment mode. * currency: The currency of the listed prices. - * expiration_datetime: The date and time after which users cannot enroll in the course in this mode. + * expiration_datetime: The date and time after which users cannot enroll in the course + in this mode. * description: A description of this mode. * invite_only: Whether students must be invited to enroll in the course; true or false. - * user: The ID of the user. + * user: The username of the user. """ authentication_classes = OAuth2AuthenticationAllowInactiveUser, EnrollmentCrossDomainSessionAuth @@ -406,21 +421,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): } ) - # Check whether any country access rules block the user from enrollment - # We do this at the view level (rather than the Python API level) - # because this check requires information about the HTTP request. - redirect_url = embargo_api.redirect_if_blocked( - course_id, user=user, ip_address=get_ip(request), url=request.path) - if redirect_url: - return Response( - status=status.HTTP_403_FORBIDDEN, - data={ - "message": ( - u"Users from this location cannot access the course '{course_id}'." - ).format(course_id=course_id), - "user_message_url": request.build_absolute_uri(redirect_url) - } - ) + embargo_response = embargo_api.get_embargo_response(request, course_id, user) + + if embargo_response: + return embargo_response try: is_active = request.DATA.get('is_active') diff --git a/common/djangoapps/lang_pref/middleware.py b/common/djangoapps/lang_pref/middleware.py index a7df9803c4..1631a871a0 100644 --- a/common/djangoapps/lang_pref/middleware.py +++ b/common/djangoapps/lang_pref/middleware.py @@ -4,6 +4,9 @@ Middleware for Language Preferences from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from lang_pref import LANGUAGE_KEY +# TODO PLAT-671 Import from Django 1.8 +# from django.utils.translation import LANGUAGE_SESSION_KEY +from django_locale.trans_real import LANGUAGE_SESSION_KEY class LanguagePreferenceMiddleware(object): @@ -16,10 +19,12 @@ class LanguagePreferenceMiddleware(object): 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 a user's UserPreference contains a language preference, use the user's preference. """ - if request.user.is_authenticated() and 'django_language' not in request.session: + # If the user is logged in, check for their language preference + if request.user.is_authenticated(): + # Get the user's language preference user_pref = get_user_preference(request.user, LANGUAGE_KEY) + # Set it to the LANGUAGE_SESSION_KEY (Django-specific session setting governing language pref) if user_pref: - request.session['django_language'] = user_pref + request.session[LANGUAGE_SESSION_KEY] = user_pref diff --git a/common/djangoapps/lang_pref/tests/test_middleware.py b/common/djangoapps/lang_pref/tests/test_middleware.py index ccc8b69a48..46934db6e1 100644 --- a/common/djangoapps/lang_pref/tests/test_middleware.py +++ b/common/djangoapps/lang_pref/tests/test_middleware.py @@ -1,6 +1,9 @@ from django.test import TestCase from django.test.client import RequestFactory from django.contrib.sessions.middleware import SessionMiddleware +# TODO PLAT-671 Import from Django 1.8 +# from django.utils.translation import LANGUAGE_SESSION_KEY +from django_locale.trans_real import LANGUAGE_SESSION_KEY from lang_pref.middleware import LanguagePreferenceMiddleware from openedx.core.djangoapps.user_api.preferences.api import set_user_preference @@ -25,19 +28,23 @@ class TestUserPreferenceMiddleware(TestCase): 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) + self.assertNotIn(LANGUAGE_SESSION_KEY, self.request.session) def test_language_in_user_prefs(self): # language set in the user preferences and not the session set_user_preference(self.user, LANGUAGE_KEY, 'eo') self.middleware.process_request(self.request) - self.assertEquals(self.request.session['django_language'], 'eo') + self.assertEquals(self.request.session[LANGUAGE_SESSION_KEY], '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' + # preference should get precedence. The session will hold the last value, + # which is probably the user's last preference. Look up the updated preference. + + # Dark lang middleware should run after this middleware, so it can + # set a session language as an override of the user's preference. + self.request.session[LANGUAGE_SESSION_KEY] = 'en' set_user_preference(self.user, LANGUAGE_KEY, 'eo') self.middleware.process_request(self.request) - self.assertEquals(self.request.session['django_language'], 'en') + self.assertEquals(self.request.session[LANGUAGE_SESSION_KEY], 'eo') diff --git a/common/djangoapps/student/cookies.py b/common/djangoapps/student/cookies.py new file mode 100644 index 0000000000..694e06f720 --- /dev/null +++ b/common/djangoapps/student/cookies.py @@ -0,0 +1,150 @@ +""" +Utility functions for setting "logged in" cookies used by subdomains. +""" + +import time +import json + +from django.utils.http import cookie_date +from django.conf import settings +from django.core.urlresolvers import reverse, NoReverseMatch + + +def set_logged_in_cookies(request, response, user): + """ + Set cookies indicating that the user is logged in. + + Some installations have an external marketing site configured + that displays a different UI when the user is logged in + (e.g. a link to the student dashboard instead of to the login page) + + Currently, two cookies are set: + + * EDXMKTG_LOGGED_IN_COOKIE_NAME: Set to 'true' if the user is logged in. + * EDXMKTG_USER_INFO_COOKIE_VERSION: JSON-encoded dictionary with user information (see below). + + The user info cookie has the following format: + { + "version": 1, + "username": "test-user", + "email": "test-user@example.com", + "header_urls": { + "account_settings": "https://example.com/account/settings", + "learner_profile": "https://example.com/u/test-user", + "logout": "https://example.com/logout" + } + } + + Arguments: + request (HttpRequest): The request to the view, used to calculate + the cookie's expiration date based on the session expiration date. + response (HttpResponse): The response on which the cookie will be set. + user (User): The currently logged in user. + + Returns: + HttpResponse + + """ + if request.session.get_expire_at_browser_close(): + max_age = None + expires = None + else: + max_age = request.session.get_expiry_age() + expires_time = time.time() + max_age + expires = cookie_date(expires_time) + + cookie_settings = { + 'max_age': max_age, + 'expires': expires, + 'domain': settings.SESSION_COOKIE_DOMAIN, + 'path': '/', + 'httponly': None, + } + + # Backwards compatibility: set the cookie indicating that the user + # is logged in. This is just a boolean value, so it's not very useful. + # In the future, we should be able to replace this with the "user info" + # cookie set below. + response.set_cookie( + settings.EDXMKTG_LOGGED_IN_COOKIE_NAME.encode('utf-8'), + 'true', + secure=None, + **cookie_settings + ) + + # Set a cookie with user info. This can be used by external sites + # to customize content based on user information. Currently, + # we include information that's used to customize the "account" + # links in the header of subdomain sites (such as the marketing site). + header_urls = {'logout': reverse('logout')} + + # Unfortunately, this app is currently used by both the LMS and Studio login pages. + # If we're in Studio, we won't be able to reverse the account/profile URLs. + # To handle this, we don't add the URLs if we can't reverse them. + # External sites will need to have fallback mechanisms to handle this case + # (most likely just hiding the links). + try: + header_urls['account_settings'] = reverse('account_settings') + header_urls['learner_profile'] = reverse('learner_profile', kwargs={'username': user.username}) + except NoReverseMatch: + pass + + # Convert relative URL paths to absolute URIs + for url_name, url_path in header_urls.iteritems(): + header_urls[url_name] = request.build_absolute_uri(url_path) + + user_info = { + 'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION, + 'username': user.username, + 'email': user.email, + 'header_urls': header_urls, + } + + # In production, TLS should be enabled so that this cookie is encrypted + # when we send it. We also need to set "secure" to True so that the browser + # will transmit it only over secure connections. + # + # In non-production environments (acceptance tests, devstack, and sandboxes), + # we still want to set this cookie. However, we do NOT want to set it to "secure" + # because the browser won't send it back to us. This can cause an infinite redirect + # loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine + # whether it needs to set the cookie or continue to the next pipeline stage. + user_info_cookie_is_secure = request.is_secure() + + response.set_cookie( + settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'), + json.dumps(user_info), + secure=user_info_cookie_is_secure, + **cookie_settings + ) + + return response + + +def delete_logged_in_cookies(response): + """ + Delete cookies indicating that the user is logged in. + + Arguments: + response (HttpResponse): The response sent to the client. + + Returns: + HttpResponse + + """ + for cookie_name in [settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, settings.EDXMKTG_USER_INFO_COOKIE_NAME]: + response.delete_cookie( + cookie_name.encode('utf-8'), + path='/', + domain=settings.SESSION_COOKIE_DOMAIN + ) + + return response + + +def is_logged_in_cookie_set(request): + """Check whether the request has logged in cookies set. """ + return ( + settings.EDXMKTG_LOGGED_IN_COOKIE_NAME in request.COOKIES and + settings.EDXMKTG_USER_INFO_COOKIE_NAME in request.COOKIES + ) diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index 5efa8f7cec..06abbe7706 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -1,54 +1,19 @@ """Helpers for the student app. """ import time from datetime import datetime +import urllib + from pytz import UTC + from django.utils.http import cookie_date from django.conf import settings from django.core.urlresolvers import reverse, NoReverseMatch + import third_party_auth -import urllib from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401 from course_modes.models import CourseMode -def set_logged_in_cookie(request, response): - """Set a cookie indicating that the user is logged in. - - Some installations have an external marketing site configured - that displays a different UI when the user is logged in - (e.g. a link to the student dashboard instead of to the login page) - - Arguments: - request (HttpRequest): The request to the view, used to calculate - the cookie's expiration date based on the session expiration date. - response (HttpResponse): The response on which the cookie will be set. - - Returns: - HttpResponse - - """ - if request.session.get_expire_at_browser_close(): - max_age = None - expires = None - else: - max_age = request.session.get_expiry_age() - expires_time = time.time() + max_age - expires = cookie_date(expires_time) - - response.set_cookie( - settings.EDXMKTG_COOKIE_NAME, 'true', max_age=max_age, - expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, - path='/', secure=None, httponly=None, - ) - - return response - - -def is_logged_in_cookie_set(request): - """Check whether the request has the logged in cookie set. """ - return settings.EDXMKTG_COOKIE_NAME in request.COOKIES - - # Enumeration of per-course verification statuses # we display on the student dashboard. VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify" @@ -231,8 +196,9 @@ def auth_pipeline_urls(auth_entry, redirect_url=None): return {} return { - provider.NAME: third_party_auth.pipeline.get_login_url(provider.NAME, auth_entry, redirect_url=redirect_url) - for provider in third_party_auth.provider.Registry.enabled() + provider.provider_id: third_party_auth.pipeline.get_login_url( + provider.provider_id, auth_entry, redirect_url=redirect_url + ) for provider in third_party_auth.provider.Registry.enabled() } diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 18c4bcc550..5335b73e7e 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -1315,6 +1315,14 @@ class CourseEnrollment(models.Model): def course(self): return modulestore().get_course(self.course_id) + @property + def course_overview(self): + """ + Return a CourseOverview of this enrollment's course. + """ + from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + return CourseOverview.get_from_id(self.course_id) + def is_verified_enrollment(self): """ Check the course enrollment mode is verified or not diff --git a/common/djangoapps/student/tests/test_change_name.py b/common/djangoapps/student/tests/test_change_name.py deleted file mode 100644 index ee6d708074..0000000000 --- a/common/djangoapps/student/tests/test_change_name.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Unit tests for change_name view of student. -""" -import json - -from django.conf import settings -from django.core.urlresolvers import reverse -from django.test.client import Client -from django.test import TestCase - -from student.tests.factories import UserFactory -from student.models import UserProfile -import unittest - - -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class TestChangeName(TestCase): - """ - Check the change_name view of student. - """ - def setUp(self): - super(TestChangeName, self).setUp() - self.student = UserFactory.create(password='test') - self.client = Client() - - def test_change_name_get_request(self): - """Get requests are not allowed in this view.""" - change_name_url = reverse('change_name') - resp = self.client.get(change_name_url) - self.assertEquals(resp.status_code, 405) - - def test_change_name_post_request(self): - """Name will be changed when provided with proper data.""" - self.client.login(username=self.student.username, password='test') - change_name_url = reverse('change_name') - resp = self.client.post(change_name_url, { - 'new_name': 'waqas', - 'rationale': 'change identity' - }) - response_data = json.loads(resp.content) - user = UserProfile.objects.get(user=self.student.id) - meta = json.loads(user.meta) - self.assertEquals(user.name, 'waqas') - self.assertEqual(meta['old_names'][0][1], 'change identity') - self.assertTrue(response_data['success']) - - def test_change_name_without_name(self): - """Empty string for name is not allowed in this view.""" - self.client.login(username=self.student.username, password='test') - change_name_url = reverse('change_name') - resp = self.client.post(change_name_url, { - 'new_name': '', - 'rationale': 'change identity' - }) - response_data = json.loads(resp.content) - self.assertFalse(response_data['success']) - - def test_unauthenticated(self): - """Unauthenticated user is not allowed to call this view.""" - change_name_url = reverse('change_name') - resp = self.client.post(change_name_url, { - 'new_name': 'waqas', - 'rationale': 'change identity' - }) - self.assertEquals(resp.status_code, 404) diff --git a/common/djangoapps/student/tests/test_create_account.py b/common/djangoapps/student/tests/test_create_account.py index f21e19bf47..e5318208ef 100644 --- a/common/djangoapps/student/tests/test_create_account.py +++ b/common/djangoapps/student/tests/test_create_account.py @@ -86,7 +86,8 @@ class TestCreateAccount(TestCase): def test_marketing_cookie(self): response = self.client.post(self.url, self.params) self.assertEqual(response.status_code, 200) - self.assertIn(settings.EDXMKTG_COOKIE_NAME, self.client.cookies) + self.assertIn(settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, self.client.cookies) + self.assertIn(settings.EDXMKTG_USER_INFO_COOKIE_NAME, self.client.cookies) @unittest.skipUnless( "microsite_configuration.middleware.MicrositeMiddleware" in settings.MIDDLEWARE_CLASSES, diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py index 2c648be6ed..4a9aa7f58c 100644 --- a/common/djangoapps/student/tests/test_login.py +++ b/common/djangoapps/student/tests/test_login.py @@ -6,6 +6,7 @@ import unittest from django.test import TestCase from django.test.client import Client +from django.test.utils import override_settings from django.conf import settings from django.core.cache import cache from django.core.urlresolvers import reverse, NoReverseMatch @@ -158,6 +159,57 @@ class LoginTest(TestCase): self.assertEqual(response.status_code, 302) self._assert_audit_log(mock_audit_log, 'info', [u'Logout', u'test']) + def test_login_user_info_cookie(self): + response, _ = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=True) + + # Verify the format of the "user info" cookie set on login + cookie = self.client.cookies[settings.EDXMKTG_USER_INFO_COOKIE_NAME] + user_info = json.loads(cookie.value) + + # Check that the version is set + self.assertEqual(user_info["version"], settings.EDXMKTG_USER_INFO_COOKIE_VERSION) + + # Check that the username and email are set + self.assertEqual(user_info["username"], self.user.username) + self.assertEqual(user_info["email"], self.user.email) + + # Check that the URLs are absolute + for url in user_info["header_urls"].values(): + self.assertIn("http://testserver/", url) + + def test_logout_deletes_mktg_cookies(self): + response, _ = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=True) + + # Check that the marketing site cookies have been set + self.assertIn(settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, self.client.cookies) + self.assertIn(settings.EDXMKTG_USER_INFO_COOKIE_NAME, self.client.cookies) + + # Log out + logout_url = reverse('logout') + response = self.client.post(logout_url) + + # Check that the marketing site cookies have been deleted + # (cookies are deleted by setting an expiration date in 1970) + for cookie_name in [settings.EDXMKTG_LOGGED_IN_COOKIE_NAME, settings.EDXMKTG_USER_INFO_COOKIE_NAME]: + cookie = self.client.cookies[cookie_name] + self.assertIn("01-Jan-1970", cookie.get('expires')) + + @override_settings( + EDXMKTG_LOGGED_IN_COOKIE_NAME=u"unicode-logged-in", + EDXMKTG_USER_INFO_COOKIE_NAME=u"unicode-user-info", + ) + def test_unicode_mktg_cookie_names(self): + # When logged in cookie names are loaded from JSON files, they may + # have type `unicode` instead of `str`, which can cause errors + # when calling Django cookie manipulation functions. + response, _ = self._login_response('test@edx.org', 'test_password') + self._assert_response(response, success=True) + + response = self.client.post(reverse('logout')) + self.assertRedirects(response, "/") + @patch.dict("django.conf.settings.FEATURES", {'SQUELCH_PII_IN_LOGS': True}) def test_logout_logging_no_pii(self): response, _ = self._login_response('test@edx.org', 'test_password') diff --git a/common/djangoapps/student/tests/test_login_registration_forms.py b/common/djangoapps/student/tests/test_login_registration_forms.py index 3ebafb2358..3fa383b0a5 100644 --- a/common/djangoapps/student/tests/test_login_registration_forms.py +++ b/common/djangoapps/student/tests/test_login_registration_forms.py @@ -11,12 +11,12 @@ from django.core.urlresolvers import reverse from util.testing import UrlResetMixin from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import CourseModeFactory +from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -# This relies on third party auth being enabled and configured -# in the test settings. See the setting `THIRD_PARTY_AUTH` -# and the feature flag `ENABLE_THIRD_PARTY_AUTH` +# This relies on third party auth being enabled in the test +# settings with the feature flag `ENABLE_THIRD_PARTY_AUTH` THIRD_PARTY_AUTH_BACKENDS = ["google-oauth2", "facebook"] THIRD_PARTY_AUTH_PROVIDERS = ["Google", "Facebook"] @@ -40,7 +40,7 @@ def _finish_auth_url(params): @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class LoginFormTest(UrlResetMixin, ModuleStoreTestCase): +class LoginFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleStoreTestCase): """Test rendering of the login form. """ @patch.dict(settings.FEATURES, {"ENABLE_COMBINED_LOGIN_REGISTRATION": False}) def setUp(self): @@ -50,6 +50,8 @@ class LoginFormTest(UrlResetMixin, ModuleStoreTestCase): self.course = CourseFactory.create() self.course_id = unicode(self.course.id) self.courseware_url = reverse("courseware", args=[self.course_id]) + self.configure_google_provider(enabled=True) + self.configure_facebook_provider(enabled=True) @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) @ddt.data(THIRD_PARTY_AUTH_PROVIDERS) @@ -148,7 +150,7 @@ class LoginFormTest(UrlResetMixin, ModuleStoreTestCase): @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class RegisterFormTest(UrlResetMixin, ModuleStoreTestCase): +class RegisterFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleStoreTestCase): """Test rendering of the registration form. """ @patch.dict(settings.FEATURES, {"ENABLE_COMBINED_LOGIN_REGISTRATION": False}) def setUp(self): @@ -157,6 +159,8 @@ class RegisterFormTest(UrlResetMixin, ModuleStoreTestCase): self.url = reverse("register_user") self.course = CourseFactory.create() self.course_id = unicode(self.course.id) + self.configure_google_provider(enabled=True) + self.configure_facebook_provider(enabled=True) @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) @ddt.data(*THIRD_PARTY_AUTH_PROVIDERS) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b71f309248..1420aa5432 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -7,8 +7,10 @@ import uuid import time import json import warnings +from datetime import timedelta from collections import defaultdict from pytz import UTC +from requests import HTTPError from ipware.ip import get_ip from django.conf import settings @@ -25,21 +27,19 @@ from django.db import IntegrityError, transaction from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError, Http404) from django.shortcuts import redirect +from django.utils import timezone from django.utils.translation import ungettext from django.utils.http import cookie_date, base36_to_int from django.utils.translation import ugettext as _, get_language from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie from django.views.decorators.http import require_POST, require_GET - from django.db.models.signals import post_save from django.dispatch import receiver - from django.template.response import TemplateResponse from ratelimitbackend.exceptions import RateLimitException -from requests import HTTPError from social.apps.django_app import utils as social_utils from social.backends import oauth as social_oauth @@ -106,9 +106,10 @@ from util.password_policy_validators import ( import third_party_auth from third_party_auth import pipeline, provider from student.helpers import ( - set_logged_in_cookie, check_verify_status_by_course, + check_verify_status_by_course, auth_pipeline_urls, get_next_url_for_login_page ) +from student.cookies import set_logged_in_cookies, delete_logged_in_cookies from student.models import anonymous_id_for_user from xmodule.error_module import ErrorDescriptor from shoppingcart.models import DonationConfiguration, CourseRegistrationCode @@ -123,13 +124,12 @@ from notification_prefs.views import enable_notifications # Note that this lives in openedx, so this dependency should be refactored. from openedx.core.djangoapps.user_api.preferences import api as preferences_api +from openedx.core.djangoapps.credit.api import get_credit_eligibility, get_purchased_credit_courses log = logging.getLogger("edx.student") AUDIT_LOG = logging.getLogger("audit") - ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=invalid-name - SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated' @@ -368,7 +368,7 @@ def signin_user(request): for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": # msg may or may not be translated. Try translating [again] in case we are able to: - third_party_auth_error = _(msg) # pylint: disable=translation-of-non-string + third_party_auth_error = _(unicode(msg)) # pylint: disable=translation-of-non-string break context = { @@ -424,10 +424,10 @@ def register_user(request, extra_context=None): # selected provider. if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) - current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend')) + current_provider = provider.Registry.get_from_pipeline(running_pipeline) overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs')) overrides['running_pipeline'] = running_pipeline - overrides['selected_provider'] = current_provider.NAME + overrides['selected_provider'] = current_provider.name context.update(overrides) return render_to_response('register.html', context) @@ -526,6 +526,13 @@ def dashboard(request): course_enrollment_pairs, course_modes_by_course ) + # Retrieve the course modes for each course + enrolled_courses_dict = {} + for course, __ in course_enrollment_pairs: + enrolled_courses_dict[unicode(course.id)] = course + + credit_messages = _create_credit_availability_message(enrolled_courses_dict, user) + course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) message = "" @@ -631,6 +638,7 @@ def dashboard(request): context = { 'enrollment_message': enrollment_message, + 'credit_messages': credit_messages, 'course_enrollment_pairs': course_enrollment_pairs, 'course_optouts': course_optouts, 'message': message, @@ -695,6 +703,47 @@ def _create_recent_enrollment_message(course_enrollment_pairs, course_modes): ) +def _create_credit_availability_message(enrolled_courses_dict, user): # pylint: disable=invalid-name + """Builds a dict of credit availability for courses. + + Construct a for courses user has completed and has not purchased credit + from the credit provider yet. + + Args: + course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information. + user (User): User object. + + Returns: + A dict of courses user is eligible for credit. + + """ + user_eligibilities = get_credit_eligibility(user.username) + user_purchased_credit = get_purchased_credit_courses(user.username) + + eligibility_messages = {} + for course_id, eligibility in user_eligibilities.iteritems(): + if course_id not in user_purchased_credit: + duration = eligibility["seconds_good_for_display"] + curr_time = timezone.now() + validity_till = eligibility["created_at"] + timedelta(seconds=duration) + if validity_till > curr_time: + diff = validity_till - curr_time + urgent = diff.days <= 30 + eligibility_messages[course_id] = { + "user_id": user.id, + "course_id": course_id, + "course_name": enrolled_courses_dict[course_id].display_name, + "providers": eligibility["providers"], + "status": eligibility["status"], + "provider": eligibility.get("provider"), + "urgent": urgent, + "user_full_name": user.get_full_name(), + "expiry": validity_till + } + + return eligibility_messages + + def _get_recently_enrolled_courses(course_enrollment_pairs): """Checks to see if the student has recently enrolled in courses. @@ -903,10 +952,11 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un running_pipeline = pipeline.get(request) username = running_pipeline['kwargs'].get('username') backend_name = running_pipeline['backend'] - requested_provider = provider.Registry.get_by_backend_name(backend_name) + third_party_uid = running_pipeline['kwargs']['uid'] + requested_provider = provider.Registry.get_from_pipeline(running_pipeline) try: - user = pipeline.get_authenticated_user(username, backend_name) + user = pipeline.get_authenticated_user(requested_provider, username, third_party_uid) third_party_auth_successful = True except User.DoesNotExist: AUDIT_LOG.warning( @@ -914,12 +964,12 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un username=username, backend_name=backend_name)) return HttpResponse( _("You've successfully logged into your {provider_name} account, but this account isn't linked with an {platform_name} account yet.").format( - platform_name=settings.PLATFORM_NAME, provider_name=requested_provider.NAME + platform_name=settings.PLATFORM_NAME, provider_name=requested_provider.name ) + "

      " + _("Use your {platform_name} username and password to log into {platform_name} below, " "and then link your {platform_name} account with {provider_name} from your dashboard.").format( - platform_name=settings.PLATFORM_NAME, provider_name=requested_provider.NAME + platform_name=settings.PLATFORM_NAME, provider_name=requested_provider.name ) + "

      " + _("If you don't have an {platform_name} account yet, click Register Now at the top of the page.").format( @@ -1067,7 +1117,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un # Ensure that the external marketing site can # detect that the user is logged in. - return set_logged_in_cookie(request, response) + return set_logged_in_cookies(request, response, user) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u"Login failed - Account not active for user.id: {0}, resending activation".format(user.id)) @@ -1131,10 +1181,8 @@ def logout_user(request): else: target = '/' response = redirect(target) - response.delete_cookie( - settings.EDXMKTG_COOKIE_NAME, - path='/', domain=settings.SESSION_COOKIE_DOMAIN, - ) + + delete_logged_in_cookies(response) return response @@ -1450,6 +1498,13 @@ def create_account_with_params(request, params): dog_stats_api.increment("common.student.account_created") + # If the user is registering via 3rd party auth, track which provider they use + third_party_provider = None + running_pipeline = None + if third_party_auth.is_enabled() and pipeline.running(request): + running_pipeline = pipeline.get(request) + third_party_provider = provider.Registry.get_from_pipeline(running_pipeline) + # Track the user's registration if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'): tracking_context = tracker.get_tracker().resolve_context() @@ -1458,20 +1513,13 @@ def create_account_with_params(request, params): 'username': user.username, }) - # If the user is registering via 3rd party auth, track which provider they use - provider_name = None - if third_party_auth.is_enabled() and pipeline.running(request): - running_pipeline = pipeline.get(request) - current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend')) - provider_name = current_provider.NAME - analytics.track( user.id, "edx.bi.user.account.registered", { 'category': 'conversion', 'label': params.get('course_id'), - 'provider': provider_name + 'provider': third_party_provider.name if third_party_provider else None }, context={ 'Google Analytics': { @@ -1488,6 +1536,7 @@ def create_account_with_params(request, params): # 2. Random user generation for other forms of testing. # 3. External auth bypassing activation. # 4. Have the platform configured to not require e-mail activation. + # 5. Registering a new user using a trusted third party provider (with skip_email_verification=True) # # Note that this feature is only tested as a flag set one way or # the other for *new* systems. we need to be careful about @@ -1496,7 +1545,11 @@ def create_account_with_params(request, params): send_email = ( not settings.FEATURES.get('SKIP_EMAIL_VALIDATION', None) and not settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING') and - not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH')) + not (do_external_auth and settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH')) and + not ( + third_party_provider and third_party_provider.skip_email_verification and + user.email == running_pipeline['kwargs'].get('details', {}).get('email') + ) ) if send_email: context = { @@ -1552,32 +1605,7 @@ def create_account_with_params(request, params): new_user.save() AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(new_user.username, new_user.email)) - -def set_marketing_cookie(request, response): - """ - Set the login cookie for the edx marketing site on the given response. Its - expiration will match that of the given request's session. - """ - if request.session.get_expire_at_browser_close(): - max_age = None - expires = None - else: - max_age = request.session.get_expiry_age() - expires_time = time.time() + max_age - expires = cookie_date(expires_time) - - # we want this cookie to be accessed via javascript - # so httponly is set to None - response.set_cookie( - settings.EDXMKTG_COOKIE_NAME, - 'true', - max_age=max_age, - expires=expires, - domain=settings.SESSION_COOKIE_DOMAIN, - path='/', - secure=None, - httponly=None - ) + return new_user @csrf_exempt @@ -1589,7 +1617,7 @@ def create_account(request, post_override=None): warnings.warn("Please use RegistrationView instead.", DeprecationWarning) try: - create_account_with_params(request, post_override or request.POST) + user = create_account_with_params(request, post_override or request.POST) except AccountValidationError as exc: return JsonResponse({'success': False, 'value': exc.message, 'field': exc.field}, status=400) except ValidationError as exc: @@ -1614,7 +1642,7 @@ def create_account(request, post_override=None): 'success': True, 'redirect_url': redirect_url, }) - set_marketing_cookie(request, response) + set_logged_in_cookies(request, response, user) return response @@ -2081,66 +2109,6 @@ def confirm_email_change(request, key): # pylint: disable=unused-argument raise -# TODO: DELETE AFTER NEW ACCOUNT PAGE DONE -@ensure_csrf_cookie -@require_POST -def change_name_request(request): - """ Log a request for a new name. """ - if not request.user.is_authenticated(): - raise Http404 - - try: - pnc = PendingNameChange.objects.get(user=request.user.id) - except PendingNameChange.DoesNotExist: - pnc = PendingNameChange() - pnc.user = request.user - pnc.new_name = request.POST['new_name'].strip() - pnc.rationale = request.POST['rationale'] - if len(pnc.new_name) < 2: - return JsonResponse({ - "success": False, - "error": _('Name required'), - }) # TODO: this should be status code 400 # pylint: disable=fixme - pnc.save() - - # The following automatically accepts name change requests. Remove this to - # go back to the old system where it gets queued up for admin approval. - accept_name_change_by_id(pnc.id) - - return JsonResponse({"success": True}) - - -# TODO: DELETE AFTER NEW ACCOUNT PAGE DONE -def accept_name_change_by_id(uid): - """ - Accepts the pending name change request for the user represented - by user id `uid`. - """ - try: - pnc = PendingNameChange.objects.get(id=uid) - except PendingNameChange.DoesNotExist: - return JsonResponse({ - "success": False, - "error": _('Invalid ID'), - }) # TODO: this should be status code 400 # pylint: disable=fixme - - user = pnc.user - u_prof = UserProfile.objects.get(user=user) - - # Save old name - meta = u_prof.get_meta() - if 'old_names' not in meta: - meta['old_names'] = [] - meta['old_names'].append([u_prof.name, pnc.rationale, datetime.datetime.now(UTC).isoformat()]) - u_prof.set_meta(meta) - - u_prof.name = pnc.new_name - u_prof.save() - pnc.delete() - - return JsonResponse({"success": True}) - - @require_POST @login_required @ensure_csrf_cookie diff --git a/common/djangoapps/third_party_auth/admin.py b/common/djangoapps/third_party_auth/admin.py new file mode 100644 index 0000000000..8495ef3a2b --- /dev/null +++ b/common/djangoapps/third_party_auth/admin.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +Admin site configuration for third party authentication +""" + +from django.contrib import admin + +from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin +from .models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, SAMLProviderData +from .tasks import fetch_saml_metadata + +admin.site.register(OAuth2ProviderConfig, KeyedConfigurationModelAdmin) + + +class SAMLProviderConfigAdmin(KeyedConfigurationModelAdmin): + """ Django Admin class for SAMLProviderConfig """ + def get_list_display(self, request): + """ Don't show every single field in the admin change list """ + return ( + 'name', 'enabled', 'backend_name', 'entity_id', 'metadata_source', + 'has_data', 'icon_class', 'change_date', 'changed_by', 'edit_link' + ) + + def has_data(self, inst): + """ Do we have cached metadata for this SAML provider? """ + if not inst.is_active: + return None # N/A + data = SAMLProviderData.current(inst.entity_id) + return bool(data and data.is_valid()) + has_data.short_description = u'Metadata Ready' + has_data.boolean = True + + def save_model(self, request, obj, form, change): + """ + Post save: Queue an asynchronous metadata fetch to update SAMLProviderData. + We only want to do this for manual edits done using the admin interface. + + Note: This only works if the celery worker and the app worker are using the + same 'configuration' cache. + """ + super(SAMLProviderConfigAdmin, self).save_model(request, obj, form, change) + fetch_saml_metadata.apply_async((), countdown=2) + +admin.site.register(SAMLProviderConfig, SAMLProviderConfigAdmin) + + +class SAMLConfigurationAdmin(ConfigurationModelAdmin): + """ Django Admin class for SAMLConfiguration """ + def get_list_display(self, request): + """ Shorten the public/private keys in the change view """ + return ( + 'change_date', 'changed_by', 'enabled', 'entity_id', + 'org_info_str', 'key_summary', + ) + + def key_summary(self, inst): + """ Short summary of the key pairs configured """ + if not inst.public_key or not inst.private_key: + return u'Key pair incomplete/missing' + pub1, pub2 = inst.public_key[0:10], inst.public_key[-10:] + priv1, priv2 = inst.private_key[0:10], inst.private_key[-10:] + return u'Public: {}…{}
      Private: {}…{}'.format(pub1, pub2, priv1, priv2) + key_summary.allow_tags = True + +admin.site.register(SAMLConfiguration, SAMLConfigurationAdmin) + + +class SAMLProviderDataAdmin(admin.ModelAdmin): + """ Django Admin class for SAMLProviderData (Read Only) """ + list_display = ('entity_id', 'is_valid', 'fetched_at', 'expires_at', 'sso_url') + readonly_fields = ('is_valid', ) + + def get_readonly_fields(self, request, obj=None): + if obj: # editing an existing object + return self.model._meta.get_all_field_names() # pylint: disable=protected-access + return self.readonly_fields + +admin.site.register(SAMLProviderData, SAMLProviderDataAdmin) diff --git a/common/djangoapps/third_party_auth/dummy.py b/common/djangoapps/third_party_auth/dummy.py index 4c2aa2dc4f..6bd8f58c4b 100644 --- a/common/djangoapps/third_party_auth/dummy.py +++ b/common/djangoapps/third_party_auth/dummy.py @@ -1,13 +1,11 @@ """ -DummyProvider: A fake Third Party Auth provider for testing & development purposes. +DummyBackend: A fake Third Party Auth provider for testing & development purposes. """ -from social.backends.base import BaseAuth +from social.backends.oauth import BaseOAuth2 from social.exceptions import AuthFailed -from .provider import BaseProvider - -class DummyBackend(BaseAuth): # pylint: disable=abstract-method +class DummyBackend(BaseOAuth2): # pylint: disable=abstract-method """ python-social-auth backend that doesn't actually go to any third party site """ @@ -47,12 +45,3 @@ class DummyBackend(BaseAuth): # pylint: disable=abstract-method kwargs.update({'response': response, 'backend': self}) return self.strategy.authenticate(*args, **kwargs) - - -class DummyProvider(BaseProvider): - """ Dummy Provider for testing and development """ - - BACKEND_CLASS = DummyBackend - ICON_CLASS = 'fa-cube' - NAME = 'Dummy' - SETTINGS = {} diff --git a/lms/djangoapps/xblock_user_state/__init__.py b/common/djangoapps/third_party_auth/management/__init__.py similarity index 100% rename from lms/djangoapps/xblock_user_state/__init__.py rename to common/djangoapps/third_party_auth/management/__init__.py diff --git a/common/djangoapps/third_party_auth/management/commands/__init__.py b/common/djangoapps/third_party_auth/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/third_party_auth/management/commands/saml.py b/common/djangoapps/third_party_auth/management/commands/saml.py new file mode 100644 index 0000000000..01918157ae --- /dev/null +++ b/common/djangoapps/third_party_auth/management/commands/saml.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Management commands for third_party_auth +""" +from django.core.management.base import BaseCommand, CommandError +import logging +from third_party_auth.models import SAMLConfiguration +from third_party_auth.tasks import fetch_saml_metadata + + +class Command(BaseCommand): + """ manage.py commands to manage SAML/Shibboleth SSO """ + help = '''Configure/maintain/update SAML-based SSO''' + + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("saml requires one argument: pull") + + if not SAMLConfiguration.is_enabled(): + raise CommandError("SAML support is disabled via SAMLConfiguration.") + + subcommand = args[0] + + if subcommand == "pull": + log_handler = logging.StreamHandler(self.stdout) + log_handler.setLevel(logging.DEBUG) + log = logging.getLogger('third_party_auth.tasks') + log.propagate = False + log.addHandler(log_handler) + num_changed, num_failed, num_total = fetch_saml_metadata() + self.stdout.write( + "\nDone. Fetched {num_total} total. {num_changed} were updated and {num_failed} failed.\n".format( + num_changed=num_changed, num_failed=num_failed, num_total=num_total + ) + ) + else: + raise CommandError("Unknown argment: {}".format(subcommand)) diff --git a/common/djangoapps/third_party_auth/migrations/0001_initial.py b/common/djangoapps/third_party_auth/migrations/0001_initial.py new file mode 100644 index 0000000000..d4a13a6dc0 --- /dev/null +++ b/common/djangoapps/third_party_auth/migrations/0001_initial.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'OAuth2ProviderConfig' + db.create_table('third_party_auth_oauth2providerconfig', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('icon_class', self.gf('django.db.models.fields.CharField')(default='fa-sign-in', max_length=50)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('backend_name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)), + ('key', self.gf('django.db.models.fields.TextField')(blank=True)), + ('secret', self.gf('django.db.models.fields.TextField')(blank=True)), + ('other_settings', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('third_party_auth', ['OAuth2ProviderConfig']) + + # Adding model 'SAMLProviderConfig' + db.create_table('third_party_auth_samlproviderconfig', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('icon_class', self.gf('django.db.models.fields.CharField')(default='fa-sign-in', max_length=50)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('backend_name', self.gf('django.db.models.fields.CharField')(default='tpa-saml', max_length=50)), + ('idp_slug', self.gf('django.db.models.fields.SlugField')(max_length=30)), + ('entity_id', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('metadata_source', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('attr_user_permanent_id', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('attr_full_name', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('attr_first_name', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('attr_last_name', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('attr_username', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('attr_email', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('other_settings', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('third_party_auth', ['SAMLProviderConfig']) + + # Adding model 'SAMLConfiguration' + db.create_table('third_party_auth_samlconfiguration', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('private_key', self.gf('django.db.models.fields.TextField')()), + ('public_key', self.gf('django.db.models.fields.TextField')()), + ('entity_id', self.gf('django.db.models.fields.CharField')(default='http://saml.example.com', max_length=255)), + ('org_info_str', self.gf('django.db.models.fields.TextField')(default='{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}')), + ('other_config_str', self.gf('django.db.models.fields.TextField')(default='{\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\n}')), + )) + db.send_create_signal('third_party_auth', ['SAMLConfiguration']) + + # Adding model 'SAMLProviderData' + db.create_table('third_party_auth_samlproviderdata', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('fetched_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True)), + ('expires_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), + ('entity_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('sso_url', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('public_key', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('third_party_auth', ['SAMLProviderData']) + + + def backwards(self, orm): + # Deleting model 'OAuth2ProviderConfig' + db.delete_table('third_party_auth_oauth2providerconfig') + + # Deleting model 'SAMLProviderConfig' + db.delete_table('third_party_auth_samlproviderconfig') + + # Deleting model 'SAMLConfiguration' + db.delete_table('third_party_auth_samlconfiguration') + + # Deleting model 'SAMLProviderData' + db.delete_table('third_party_auth_samlproviderdata') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'third_party_auth.oauth2providerconfig': { + 'Meta': {'object_name': 'OAuth2ProviderConfig'}, + 'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'third_party_auth.samlconfiguration': { + 'Meta': {'object_name': 'SAMLConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}), + 'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}), + 'private_key': ('django.db.models.fields.TextField', [], {}), + 'public_key': ('django.db.models.fields.TextField', [], {}) + }, + 'third_party_auth.samlproviderconfig': { + 'Meta': {'object_name': 'SAMLProviderConfig'}, + 'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}), + 'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'third_party_auth.samlproviderdata': { + 'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'}, + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_key': ('django.db.models.fields.TextField', [], {}), + 'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + } + } + + complete_apps = ['third_party_auth'] \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/migrations/0002_convert_settings.py b/common/djangoapps/third_party_auth/migrations/0002_convert_settings.py new file mode 100644 index 0000000000..a5c38bca81 --- /dev/null +++ b/common/djangoapps/third_party_auth/migrations/0002_convert_settings.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +from django.conf import settings +import json +from south.v2 import DataMigration + + +class Migration(DataMigration): + + def forwards(self, orm): + """ Convert from the THIRD_PARTY_AUTH setting to OAuth2ProviderConfig """ + tpa = getattr(settings, 'THIRD_PARTY_AUTH_OLD_CONFIG', {}) + if tpa and not any(orm.OAuth2ProviderConfig.objects.all()): + print("Migrating third party auth config to OAuth2ProviderConfig") + providers = ( + # Name, backend, icon, prefix + ('Google', 'google-oauth2', 'fa-google-plus', 'SOCIAL_AUTH_GOOGLE_OAUTH2_'), + ('LinkedIn', 'linkedin-oauth2', 'fa-linkedin', 'SOCIAL_AUTH_LINKEDIN_OAUTH2_'), + ('Facebook', 'facebook', 'fa-facebook', 'SOCIAL_AUTH_FACEBOOK_'), + ) + for name, backend, icon, prefix in providers: + if name in tpa: + conf = tpa[name] + conf = {key.replace(prefix, ''): val for key, val in conf.items()} + key = conf.pop('KEY', '') + secret = conf.pop('SECRET', '') + orm.OAuth2ProviderConfig.objects.create( + enabled=True, + name=name, + backend_name=backend, + icon_class=icon, + key=key, + secret=secret, + other_settings=json.dumps(conf), + changed_by=None, + ) + print( + "Done. Make changes via /admin/third_party_auth/oauth2providerconfig/ " + "from now on. You can remove THIRD_PARTY_AUTH from ~/lms.auth.json" + ) + else: + print("Not migrating third party auth config to OAuth2ProviderConfig.") + + def backwards(self, orm): + """ No backwards migration necessary """ + pass + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'third_party_auth.oauth2providerconfig': { + 'Meta': {'object_name': 'OAuth2ProviderConfig'}, + 'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'third_party_auth.samlconfiguration': { + 'Meta': {'object_name': 'SAMLConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}), + 'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}), + 'private_key': ('django.db.models.fields.TextField', [], {}), + 'public_key': ('django.db.models.fields.TextField', [], {}) + }, + 'third_party_auth.samlproviderconfig': { + 'Meta': {'object_name': 'SAMLProviderConfig'}, + 'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}), + 'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'third_party_auth.samlproviderdata': { + 'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'}, + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_key': ('django.db.models.fields.TextField', [], {}), + 'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + } + } + + complete_apps = ['third_party_auth'] + symmetrical = True diff --git a/common/djangoapps/third_party_auth/migrations/0003_add_config_options.py b/common/djangoapps/third_party_auth/migrations/0003_add_config_options.py new file mode 100644 index 0000000000..6ff8a3d3a5 --- /dev/null +++ b/common/djangoapps/third_party_auth/migrations/0003_add_config_options.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'SAMLProviderConfig.secondary' + db.add_column('third_party_auth_samlproviderconfig', 'secondary', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'SAMLProviderConfig.skip_registration_form' + db.add_column('third_party_auth_samlproviderconfig', 'skip_registration_form', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'SAMLProviderConfig.skip_email_verification' + db.add_column('third_party_auth_samlproviderconfig', 'skip_email_verification', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'OAuth2ProviderConfig.secondary' + db.add_column('third_party_auth_oauth2providerconfig', 'secondary', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'OAuth2ProviderConfig.skip_registration_form' + db.add_column('third_party_auth_oauth2providerconfig', 'skip_registration_form', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'OAuth2ProviderConfig.skip_email_verification' + db.add_column('third_party_auth_oauth2providerconfig', 'skip_email_verification', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'SAMLProviderConfig.secondary' + db.delete_column('third_party_auth_samlproviderconfig', 'secondary') + + # Deleting field 'SAMLProviderConfig.skip_registration_form' + db.delete_column('third_party_auth_samlproviderconfig', 'skip_registration_form') + + # Deleting field 'SAMLProviderConfig.skip_email_verification' + db.delete_column('third_party_auth_samlproviderconfig', 'skip_email_verification') + + # Deleting field 'OAuth2ProviderConfig.secondary' + db.delete_column('third_party_auth_oauth2providerconfig', 'secondary') + + # Deleting field 'OAuth2ProviderConfig.skip_registration_form' + db.delete_column('third_party_auth_oauth2providerconfig', 'skip_registration_form') + + # Deleting field 'OAuth2ProviderConfig.skip_email_verification' + db.delete_column('third_party_auth_oauth2providerconfig', 'skip_email_verification') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'third_party_auth.oauth2providerconfig': { + 'Meta': {'object_name': 'OAuth2ProviderConfig'}, + 'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'third_party_auth.samlconfiguration': { + 'Meta': {'object_name': 'SAMLConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}), + 'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}), + 'private_key': ('django.db.models.fields.TextField', [], {}), + 'public_key': ('django.db.models.fields.TextField', [], {}) + }, + 'third_party_auth.samlproviderconfig': { + 'Meta': {'object_name': 'SAMLProviderConfig'}, + 'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}), + 'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'third_party_auth.samlproviderdata': { + 'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'}, + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_key': ('django.db.models.fields.TextField', [], {}), + 'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + } + } + + complete_apps = ['third_party_auth'] \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/migrations/__init__.py b/common/djangoapps/third_party_auth/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/third_party_auth/models.py b/common/djangoapps/third_party_auth/models.py new file mode 100644 index 0000000000..1550c54eba --- /dev/null +++ b/common/djangoapps/third_party_auth/models.py @@ -0,0 +1,420 @@ +# -*- coding: utf-8 -*- +""" +Models used to implement SAML SSO support in third_party_auth +(inlcuding Shibboleth support) +""" +from config_models.models import ConfigurationModel, cache +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +import json +import logging +from social.backends.base import BaseAuth +from social.backends.oauth import BaseOAuth2 +from social.backends.saml import SAMLAuth, SAMLIdentityProvider +from social.exceptions import SocialAuthBaseException +from social.utils import module_member + +log = logging.getLogger(__name__) + + +# A dictionary of {name: class} entries for each python-social-auth backend available. +# Because this setting can specify arbitrary code to load and execute, it is set via +# normal Django settings only and cannot be changed at runtime: +def _load_backend_classes(base_class=BaseAuth): + """ Load the list of python-social-auth backend classes from Django settings """ + for class_path in settings.AUTHENTICATION_BACKENDS: + auth_class = module_member(class_path) + if issubclass(auth_class, base_class): + yield auth_class +_PSA_BACKENDS = {backend_class.name: backend_class for backend_class in _load_backend_classes()} +_PSA_OAUTH2_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(BaseOAuth2)] +_PSA_SAML_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(SAMLAuth)] + + +def clean_json(value, of_type): + """ Simple helper method to parse and clean JSON """ + if not value.strip(): + return json.dumps(of_type()) + try: + value_python = json.loads(value) + except ValueError as err: + raise ValidationError("Invalid JSON: {}".format(err.message)) + if not isinstance(value_python, of_type): + raise ValidationError("Expected a JSON {}".format(of_type)) + return json.dumps(value_python, indent=4) + + +class AuthNotConfigured(SocialAuthBaseException): + """ Exception when SAMLProviderData or other required info is missing """ + def __init__(self, provider_name): + super(AuthNotConfigured, self).__init__() + self.provider_name = provider_name + + def __str__(self): + return _('Authentication with {} is currently unavailable.').format( # pylint: disable=no-member + self.provider_name + ) + + +class ProviderConfig(ConfigurationModel): + """ + Abstract Base Class for configuring a third_party_auth provider + """ + icon_class = models.CharField( + max_length=50, default='fa-sign-in', + help_text=( + 'The Font Awesome (or custom) icon class to use on the login button for this provider. ' + 'Examples: fa-google-plus, fa-facebook, fa-linkedin, fa-sign-in, fa-university' + ), + ) + name = models.CharField(max_length=50, blank=False, help_text="Name of this provider (shown to users)") + secondary = models.BooleanField( + default=False, + help_text=_( + 'Secondary providers are displayed less prominently, ' + 'in a separate list of "Institution" login providers.' + ), + ) + skip_registration_form = models.BooleanField( + default=False, + help_text=_( + "If this option is enabled, users will not be asked to confirm their details " + "(name, email, etc.) during the registration process. Only select this option " + "for trusted providers that are known to provide accurate user information." + ), + ) + skip_email_verification = models.BooleanField( + default=False, + help_text=_( + "If this option is selected, users will not be required to confirm their " + "email, and their account will be activated immediately upon registration." + ), + ) + prefix = None # used for provider_id. Set to a string value in subclass + backend_name = None # Set to a field or fixed value in subclass + + # "enabled" field is inherited from ConfigurationModel + + class Meta(object): # pylint: disable=missing-docstring + abstract = True + + @property + def provider_id(self): + """ Unique string key identifying this provider. Must be URL and css class friendly. """ + assert self.prefix is not None + return "-".join((self.prefix, ) + tuple(getattr(self, field) for field in self.KEY_FIELDS)) + + @property + def backend_class(self): + """ Get the python-social-auth backend class used for this provider """ + return _PSA_BACKENDS[self.backend_name] + + def get_url_params(self): + """ Get a dict of GET parameters to append to login links for this provider """ + return {} + + def is_active_for_pipeline(self, pipeline): + """ Is this provider being used for the specified pipeline? """ + return self.backend_name == pipeline['backend'] + + def match_social_auth(self, social_auth): + """ Is this provider being used for this UserSocialAuth entry? """ + return self.backend_name == social_auth.provider + + @classmethod + def get_register_form_data(cls, pipeline_kwargs): + """Gets dict of data to display on the register form. + + common.djangoapps.student.views.register_user uses this to populate the + new account creation form with values supplied by the user's chosen + provider, preventing duplicate data entry. + + Args: + pipeline_kwargs: dict of string -> object. Keyword arguments + accumulated by the pipeline thus far. + + Returns: + Dict of string -> string. Keys are names of form fields; values are + values for that field. Where there is no value, the empty string + must be used. + """ + # Details about the user sent back from the provider. + details = pipeline_kwargs.get('details') + + # Get the username separately to take advantage of the de-duping logic + # built into the pipeline. The provider cannot de-dupe because it can't + # check the state of taken usernames in our system. Note that there is + # technically a data race between the creation of this value and the + # creation of the user object, so it is still possible for users to get + # an error on submit. + suggested_username = pipeline_kwargs.get('username') + + return { + 'email': details.get('email', ''), + 'name': details.get('fullname', ''), + 'username': suggested_username, + } + + def get_authentication_backend(self): + """Gets associated Django settings.AUTHENTICATION_BACKEND string.""" + return '{}.{}'.format(self.backend_class.__module__, self.backend_class.__name__) + + +class OAuth2ProviderConfig(ProviderConfig): + """ + Configuration Entry for an OAuth2 based provider. + """ + prefix = 'oa2' + KEY_FIELDS = ('backend_name', ) # Backend name is unique + backend_name = models.CharField( + max_length=50, choices=[(name, name) for name in _PSA_OAUTH2_BACKENDS], blank=False, db_index=True, + help_text=( + "Which python-social-auth OAuth2 provider backend to use. " + "The list of backend choices is determined by the THIRD_PARTY_AUTH_BACKENDS setting." + # To be precise, it's set by AUTHENTICATION_BACKENDS - which aws.py sets from THIRD_PARTY_AUTH_BACKENDS + ) + ) + key = models.TextField(blank=True, verbose_name="Client ID") + secret = models.TextField(blank=True, verbose_name="Client Secret") + other_settings = models.TextField(blank=True, help_text="Optional JSON object with advanced settings, if any.") + + class Meta(object): # pylint: disable=missing-docstring + verbose_name = "Provider Configuration (OAuth2)" + verbose_name_plural = verbose_name + + def clean(self): + """ Standardize and validate fields """ + super(OAuth2ProviderConfig, self).clean() + self.other_settings = clean_json(self.other_settings, dict) + + def get_setting(self, name): + """ Get the value of a setting, or raise KeyError """ + if name in ("KEY", "SECRET"): + return getattr(self, name.lower()) + if self.other_settings: + other_settings = json.loads(self.other_settings) + assert isinstance(other_settings, dict), "other_settings should be a JSON object (dictionary)" + return other_settings[name] + raise KeyError + + +class SAMLProviderConfig(ProviderConfig): + """ + Configuration Entry for a SAML/Shibboleth provider. + """ + prefix = 'saml' + KEY_FIELDS = ('idp_slug', ) + backend_name = models.CharField( + max_length=50, default='tpa-saml', choices=[(name, name) for name in _PSA_SAML_BACKENDS], blank=False, + help_text="Which python-social-auth provider backend to use. 'tpa-saml' is the standard edX SAML backend.") + idp_slug = models.SlugField( + max_length=30, db_index=True, + help_text=( + 'A short string uniquely identifying this provider. ' + 'Cannot contain spaces and should be a usable as a CSS class. Examples: "ubc", "mit-staging"' + )) + entity_id = models.CharField( + max_length=255, verbose_name="Entity ID", help_text="Example: https://idp.testshib.org/idp/shibboleth") + metadata_source = models.CharField( + max_length=255, + help_text=( + "URL to this provider's XML metadata. Should be an HTTPS URL. " + "Example: https://www.testshib.org/metadata/testshib-providers.xml" + )) + attr_user_permanent_id = models.CharField( + max_length=128, blank=True, verbose_name="User ID Attribute", + help_text="URN of the SAML attribute that we can use as a unique, persistent user ID. Leave blank for default.") + attr_full_name = models.CharField( + max_length=128, blank=True, verbose_name="Full Name Attribute", + help_text="URN of SAML attribute containing the user's full name. Leave blank for default.") + attr_first_name = models.CharField( + max_length=128, blank=True, verbose_name="First Name Attribute", + help_text="URN of SAML attribute containing the user's first name. Leave blank for default.") + attr_last_name = models.CharField( + max_length=128, blank=True, verbose_name="Last Name Attribute", + help_text="URN of SAML attribute containing the user's last name. Leave blank for default.") + attr_username = models.CharField( + max_length=128, blank=True, verbose_name="Username Hint Attribute", + help_text="URN of SAML attribute to use as a suggested username for this user. Leave blank for default.") + attr_email = models.CharField( + max_length=128, blank=True, verbose_name="Email Attribute", + help_text="URN of SAML attribute containing the user's email address[es]. Leave blank for default.") + other_settings = models.TextField( + verbose_name="Advanced settings", blank=True, + help_text=( + 'For advanced use cases, enter a JSON object with addtional configuration. ' + 'The tpa-saml backend supports only {"requiredEntitlements": ["urn:..."]} ' + 'which can be used to require the presence of a specific eduPersonEntitlement.' + )) + + def clean(self): + """ Standardize and validate fields """ + super(SAMLProviderConfig, self).clean() + self.other_settings = clean_json(self.other_settings, dict) + + class Meta(object): # pylint: disable=missing-docstring + verbose_name = "Provider Configuration (SAML IdP)" + verbose_name_plural = "Provider Configuration (SAML IdPs)" + + def get_url_params(self): + """ Get a dict of GET parameters to append to login links for this provider """ + return {'idp': self.idp_slug} + + def is_active_for_pipeline(self, pipeline): + """ Is this provider being used for the specified pipeline? """ + return self.backend_name == pipeline['backend'] and self.idp_slug == pipeline['kwargs']['response']['idp_name'] + + def match_social_auth(self, social_auth): + """ Is this provider being used for this UserSocialAuth entry? """ + prefix = self.idp_slug + ":" + return self.backend_name == social_auth.provider and social_auth.uid.startswith(prefix) + + def get_config(self): + """ + Return a SAMLIdentityProvider instance for use by SAMLAuthBackend. + + Essentially this just returns the values of this object and its + associated 'SAMLProviderData' entry. + """ + if self.other_settings: + conf = json.loads(self.other_settings) + else: + conf = {} + attrs = ( + 'attr_user_permanent_id', 'attr_full_name', 'attr_first_name', + 'attr_last_name', 'attr_username', 'attr_email', 'entity_id') + for field in attrs: + val = getattr(self, field) + if val: + conf[field] = val + # Now get the data fetched automatically from the metadata.xml: + data = SAMLProviderData.current(self.entity_id) + if not data or not data.is_valid(): + log.error("No SAMLProviderData found for %s. Run 'manage.py saml pull' to fix or debug.", self.entity_id) + raise AuthNotConfigured(provider_name=self.name) + conf['x509cert'] = data.public_key + conf['url'] = data.sso_url + return SAMLIdentityProvider(self.idp_slug, **conf) + + +class SAMLConfiguration(ConfigurationModel): + """ + General configuration required for this edX instance to act as a SAML + Service Provider and allow users to authenticate via third party SAML + Identity Providers (IdPs) + """ + private_key = models.TextField( + help_text=( + 'To generate a key pair as two files, run ' + '"openssl req -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.key". ' + 'Paste the contents of saml.key here.' + ) + ) + public_key = models.TextField(help_text="Public key certificate.") + entity_id = models.CharField(max_length=255, default="http://saml.example.com", verbose_name="Entity ID") + org_info_str = models.TextField( + verbose_name="Organization Info", + default='{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}', + help_text="JSON dictionary of 'url', 'displayname', and 'name' for each language", + ) + other_config_str = models.TextField( + default='{\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\n}', + help_text=( + "JSON object defining advanced settings that are passed on to python-saml. " + "Valid keys that can be set here include: SECURITY_CONFIG and SP_EXTRA" + ), + ) + + class Meta(object): # pylint: disable=missing-docstring + verbose_name = "SAML Configuration" + verbose_name_plural = verbose_name + + def clean(self): + """ Standardize and validate fields """ + super(SAMLConfiguration, self).clean() + self.org_info_str = clean_json(self.org_info_str, dict) + self.other_config_str = clean_json(self.other_config_str, dict) + + self.private_key = ( + self.private_key + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END RSA PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .strip() + ) + self.public_key = ( + self.public_key + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .strip() + ) + + def get_setting(self, name): + """ Get the value of a setting, or raise KeyError """ + if name == "ORG_INFO": + return json.loads(self.org_info_str) + if name == "SP_ENTITY_ID": + return self.entity_id + if name == "SP_PUBLIC_CERT": + return self.public_key + if name == "SP_PRIVATE_KEY": + return self.private_key + if name == "TECHNICAL_CONTACT": + return {"givenName": "Technical Support", "emailAddress": settings.TECH_SUPPORT_EMAIL} + if name == "SUPPORT_CONTACT": + return {"givenName": "SAML Support", "emailAddress": settings.TECH_SUPPORT_EMAIL} + other_config = json.loads(self.other_config_str) + return other_config[name] # SECURITY_CONFIG, SP_EXTRA, or similar extra settings + + +class SAMLProviderData(models.Model): + """ + Data about a SAML IdP that is fetched automatically by 'manage.py saml pull' + + This data is only required during the actual authentication process. + """ + cache_timeout = 600 + fetched_at = models.DateTimeField(db_index=True, null=False) + expires_at = models.DateTimeField(db_index=True, null=True) + + entity_id = models.CharField(max_length=255, db_index=True) # This is the key for lookups in this table + sso_url = models.URLField(verbose_name="SSO URL") + public_key = models.TextField() + + class Meta(object): # pylint: disable=missing-docstring + verbose_name = "SAML Provider Data" + verbose_name_plural = verbose_name + ordering = ('-fetched_at', ) + + def is_valid(self): + """ Is this data valid? """ + if self.expires_at and timezone.now() > self.expires_at: + return False + return bool(self.entity_id and self.sso_url and self.public_key) + is_valid.boolean = True + + @classmethod + def cache_key_name(cls, entity_id): + """ Return the name of the key to use to cache the current data """ + return 'configuration/{}/current/{}'.format(cls.__name__, entity_id) + + @classmethod + def current(cls, entity_id): + """ + Return the active data entry, if any, otherwise None + """ + cached = cache.get(cls.cache_key_name(entity_id)) + if cached is not None: + return cached + + try: + current = cls.objects.filter(entity_id=entity_id).order_by('-fetched_at')[0] + except IndexError: + current = None + + cache.set(cls.cache_key_name(entity_id), current, cls.cache_timeout) + return current diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index 4d4ba9ba99..5bc4f069dd 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -61,7 +61,6 @@ import random import string # pylint: disable-msg=deprecated-module from collections import OrderedDict import urllib -from ipware.ip import get_ip import analytics from eventtracking import tracker @@ -197,9 +196,11 @@ class ProviderUserState(object): lms/templates/dashboard.html. """ - def __init__(self, enabled_provider, user, state): + def __init__(self, enabled_provider, user, association_id=None): + # UserSocialAuth row ID + self.association_id = association_id # Boolean. Whether the user has an account associated with the provider - self.has_account = state + self.has_account = association_id is not None # provider.BaseProvider child. Callers must verify that the provider is # enabled. self.provider = enabled_provider @@ -208,7 +209,7 @@ class ProviderUserState(object): def get_unlink_form_name(self): """Gets the name used in HTML forms that unlink a provider account.""" - return self.provider.NAME + '_unlink_form' + return self.provider.provider_id + '_unlink_form' def get(request): @@ -216,7 +217,7 @@ def get(request): return request.session.get('partial_pipeline') -def get_authenticated_user(username, backend_name): +def get_authenticated_user(auth_provider, username, uid): """Gets a saved user authenticated by a particular backend. Between pipeline steps User objects are not saved. We need to reconstitute @@ -225,43 +226,45 @@ def get_authenticated_user(username, backend_name): authenticate(). Args: + auth_provider: the third_party_auth provider in use for the current pipeline. username: string. Username of user to get. - backend_name: string. The name of the third-party auth backend from - the running pipeline. + uid: string. The user ID according to the third party. Returns: User if user is found and has a social auth from the passed - backend_name. + provider. Raises: User.DoesNotExist: if no user matching user is found, or the matching user has no social auth associated with the given backend. AssertionError: if the user is not authenticated. """ - user = models.DjangoStorage.user.user_model().objects.get(username=username) - match = models.DjangoStorage.user.get_social_auth_for_user(user, provider=backend_name) + match = models.DjangoStorage.user.get_social_auth(provider=auth_provider.backend_name, uid=uid) - if not match: + if not match or match.user.username != username: raise User.DoesNotExist - user.backend = provider.Registry.get_by_backend_name(backend_name).get_authentication_backend() + user = match.user + user.backend = auth_provider.get_authentication_backend() return user -def _get_enabled_provider_by_name(provider_name): - """Gets an enabled provider by its NAME member or throws.""" - enabled_provider = provider.Registry.get(provider_name) +def _get_enabled_provider(provider_id): + """Gets an enabled provider by its provider_id member or throws.""" + enabled_provider = provider.Registry.get(provider_id) if not enabled_provider: - raise ValueError('Provider %s not enabled' % provider_name) + raise ValueError('Provider %s not enabled' % provider_id) return enabled_provider -def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None): +def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None, + extra_params=None, url_params=None): """Creates a URL to hook into social auth endpoints.""" - kwargs = {'backend': backend_name} - url = reverse(view_name, kwargs=kwargs) + url_params = url_params or {} + url_params['backend'] = backend_name + url = reverse(view_name, kwargs=url_params) query_params = OrderedDict() if auth_entry: @@ -270,6 +273,9 @@ def _get_url(view_name, backend_name, auth_entry=None, redirect_url=None): if redirect_url: query_params[AUTH_REDIRECT_KEY] = redirect_url + if extra_params: + query_params.update(extra_params) + return u"{url}?{params}".format( url=url, params=urllib.urlencode(query_params) @@ -289,37 +295,40 @@ def get_complete_url(backend_name): Raises: ValueError: if no provider is enabled with the given backend_name. """ - enabled_provider = provider.Registry.get_by_backend_name(backend_name) - - if not enabled_provider: + if not any(provider.Registry.get_enabled_by_backend_name(backend_name)): raise ValueError('Provider with backend %s not enabled' % backend_name) return _get_url('social:complete', backend_name) -def get_disconnect_url(provider_name): +def get_disconnect_url(provider_id, association_id): """Gets URL for the endpoint that starts the disconnect pipeline. Args: - provider_name: string. Name of the provider.BaseProvider child you want + provider_id: string identifier of the models.ProviderConfig child you want to disconnect from. + association_id: int. Optional ID of a specific row in the UserSocialAuth + table to disconnect (useful if multiple providers use a common backend) Returns: String. URL that starts the disconnection pipeline. Raises: - ValueError: if no provider is enabled with the given backend_name. + ValueError: if no provider is enabled with the given ID. """ - enabled_provider = _get_enabled_provider_by_name(provider_name) - return _get_url('social:disconnect', enabled_provider.BACKEND_CLASS.name) + backend_name = _get_enabled_provider(provider_id).backend_name + if association_id: + return _get_url('social:disconnect_individual', backend_name, url_params={'association_id': association_id}) + else: + return _get_url('social:disconnect', backend_name) -def get_login_url(provider_name, auth_entry, redirect_url=None): +def get_login_url(provider_id, auth_entry, redirect_url=None): """Gets the login URL for the endpoint that kicks off auth with a provider. Args: - provider_name: string. The name of the provider.Provider that has been - enabled. + provider_id: string identifier of the models.ProviderConfig child you want + to disconnect from. auth_entry: string. Query argument specifying the desired entry point for the auth pipeline. Used by the pipeline for later branching. Must be one of _AUTH_ENTRY_CHOICES. @@ -332,15 +341,16 @@ def get_login_url(provider_name, auth_entry, redirect_url=None): String. URL that starts the auth pipeline for a provider. Raises: - ValueError: if no provider is enabled with the given provider_name. + ValueError: if no provider is enabled with the given provider_id. """ assert auth_entry in _AUTH_ENTRY_CHOICES - enabled_provider = _get_enabled_provider_by_name(provider_name) + enabled_provider = _get_enabled_provider(provider_id) return _get_url( 'social:begin', - enabled_provider.BACKEND_CLASS.name, + enabled_provider.backend_name, auth_entry=auth_entry, redirect_url=redirect_url, + extra_params=enabled_provider.get_url_params(), ) @@ -356,7 +366,7 @@ def get_duplicate_provider(messages): unfortunately not in a reusable constant. Returns: - provider.BaseProvider child instance. The provider of the duplicate + string name of the python-social-auth backend that has the duplicate account, or None if there is no duplicate (and hence no error). """ social_auth_messages = [m for m in messages if m.message.endswith('is already in use.')] @@ -365,7 +375,8 @@ def get_duplicate_provider(messages): return assert len(social_auth_messages) == 1 - return provider.Registry.get_by_backend_name(social_auth_messages[0].extra_tags.split()[1]) + backend_name = social_auth_messages[0].extra_tags.split()[1] + return backend_name def get_provider_user_states(user): @@ -379,13 +390,16 @@ def get_provider_user_states(user): each enabled provider. """ states = [] - found_user_backends = [ - social_auth.provider for social_auth in models.DjangoStorage.user.get_social_auth_for_user(user) - ] + found_user_auths = list(models.DjangoStorage.user.get_social_auth_for_user(user)) for enabled_provider in provider.Registry.enabled(): + association_id = None + for auth in found_user_auths: + if enabled_provider.match_social_auth(auth): + association_id = auth.id + break states.append( - ProviderUserState(enabled_provider, user, enabled_provider.BACKEND_CLASS.name in found_user_backends) + ProviderUserState(enabled_provider, user, association_id) ) return states @@ -489,12 +503,19 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia """Redirects to the registration page.""" return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER]) + def should_force_account_creation(): + """ For some third party providers, we auto-create user accounts """ + current_provider = provider.Registry.get_from_pipeline({'backend': backend.name, 'kwargs': kwargs}) + return current_provider and current_provider.skip_email_verification + if not user: if auth_entry in [AUTH_ENTRY_LOGIN_API, AUTH_ENTRY_REGISTER_API]: return HttpResponseBadRequest() elif auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]: # User has authenticated with the third party provider but we don't know which edX # account corresponds to them yet, if any. + if should_force_account_creation(): + return dispatch_to_register() return dispatch_to_login() elif auth_entry in [AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER_2]: # User has authenticated with the third party provider and now wants to finish @@ -534,7 +555,7 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia @partial.partial -def set_logged_in_cookie(backend=None, user=None, strategy=None, auth_entry=None, *args, **kwargs): +def set_logged_in_cookies(backend=None, user=None, strategy=None, auth_entry=None, *args, **kwargs): """This pipeline step sets the "logged in" cookie for authenticated users. Some installations have a marketing site front-end separate from @@ -566,7 +587,7 @@ def set_logged_in_cookie(backend=None, user=None, strategy=None, auth_entry=None # Check that the cookie isn't already set. # This ensures that we allow the user to continue to the next # pipeline step once he/she has the cookie set by this step. - has_cookie = student.helpers.is_logged_in_cookie_set(request) + has_cookie = student.cookies.is_logged_in_cookie_set(request) if not has_cookie: try: redirect_url = get_complete_url(backend.name) @@ -577,7 +598,7 @@ def set_logged_in_cookie(backend=None, user=None, strategy=None, auth_entry=None pass else: response = redirect(redirect_url) - return student.helpers.set_logged_in_cookie(request, response) + return student.cookies.set_logged_in_cookies(request, response, user) @partial.partial diff --git a/common/djangoapps/third_party_auth/provider.py b/common/djangoapps/third_party_auth/provider.py index 9f0809d42a..415e670900 100644 --- a/common/djangoapps/third_party_auth/provider.py +++ b/common/djangoapps/third_party_auth/provider.py @@ -1,234 +1,85 @@ -"""Third-party auth provider definitions. - -Loaded by Django's settings mechanism. Consequently, this module must not -invoke the Django armature. """ - -from social.backends import google, linkedin, facebook - -_DEFAULT_ICON_CLASS = 'fa-signin' - - -class BaseProvider(object): - """Abstract base class for third-party auth providers. - - All providers must subclass BaseProvider -- otherwise, they cannot be put - in the provider Registry. - """ - - # Class. The provider's backing social.backends.base.BaseAuth child. - BACKEND_CLASS = None - # String. Name of the FontAwesome glyph to use for sign in buttons (or the - # name of a user-supplied custom glyph that is present at runtime). - ICON_CLASS = _DEFAULT_ICON_CLASS - # String. User-facing name of the provider. Must be unique across all - # enabled providers. Will be presented in the UI. - NAME = None - # Dict of string -> object. Settings that will be merged into Django's - # settings instance. In most cases the value will be None, since real - # values are merged from .json files (foo.auth.json; foo.env.json) onto the - # settings instance during application initialization. - SETTINGS = {} - - @classmethod - def get_authentication_backend(cls): - """Gets associated Django settings.AUTHENTICATION_BACKEND string.""" - return '%s.%s' % (cls.BACKEND_CLASS.__module__, cls.BACKEND_CLASS.__name__) - - @classmethod - def get_email(cls, provider_details): - """Gets user's email address. - - Provider responses can contain arbitrary data. This method can be - overridden to extract an email address from the provider details - extracted by the social_details pipeline step. - - Args: - provider_details: dict of string -> string. Data about the - user passed back by the provider. - - Returns: - String or None. The user's email address, if any. - """ - return provider_details.get('email') - - @classmethod - def get_name(cls, provider_details): - """Gets user's name. - - Provider responses can contain arbitrary data. This method can be - overridden to extract a full name for a user from the provider details - extracted by the social_details pipeline step. - - Args: - provider_details: dict of string -> string. Data about the - user passed back by the provider. - - Returns: - String or None. The user's full name, if any. - """ - return provider_details.get('fullname') - - @classmethod - def get_register_form_data(cls, pipeline_kwargs): - """Gets dict of data to display on the register form. - - common.djangoapps.student.views.register_user uses this to populate the - new account creation form with values supplied by the user's chosen - provider, preventing duplicate data entry. - - Args: - pipeline_kwargs: dict of string -> object. Keyword arguments - accumulated by the pipeline thus far. - - Returns: - Dict of string -> string. Keys are names of form fields; values are - values for that field. Where there is no value, the empty string - must be used. - """ - # Details about the user sent back from the provider. - details = pipeline_kwargs.get('details') - - # Get the username separately to take advantage of the de-duping logic - # built into the pipeline. The provider cannot de-dupe because it can't - # check the state of taken usernames in our system. Note that there is - # technically a data race between the creation of this value and the - # creation of the user object, so it is still possible for users to get - # an error on submit. - suggested_username = pipeline_kwargs.get('username') - - return { - 'email': cls.get_email(details) or '', - 'name': cls.get_name(details) or '', - 'username': suggested_username, - } - - @classmethod - def merge_onto(cls, settings): - """Merge class-level settings onto a django settings module.""" - for key, value in cls.SETTINGS.iteritems(): - setattr(settings, key, value) - - -class GoogleOauth2(BaseProvider): - """Provider for Google's Oauth2 auth system.""" - - BACKEND_CLASS = google.GoogleOAuth2 - ICON_CLASS = 'fa-google-plus' - NAME = 'Google' - SETTINGS = { - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': None, - 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': None, - } - - -class LinkedInOauth2(BaseProvider): - """Provider for LinkedIn's Oauth2 auth system.""" - - BACKEND_CLASS = linkedin.LinkedinOAuth2 - ICON_CLASS = 'fa-linkedin' - NAME = 'LinkedIn' - SETTINGS = { - 'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': None, - 'SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET': None, - } - - -class FacebookOauth2(BaseProvider): - """Provider for LinkedIn's Oauth2 auth system.""" - - BACKEND_CLASS = facebook.FacebookOAuth2 - ICON_CLASS = 'fa-facebook' - NAME = 'Facebook' - SETTINGS = { - 'SOCIAL_AUTH_FACEBOOK_KEY': None, - 'SOCIAL_AUTH_FACEBOOK_SECRET': None, - } +Third-party auth provider configuration API. +""" +from .models import ( + OAuth2ProviderConfig, SAMLConfiguration, SAMLProviderConfig, + _PSA_OAUTH2_BACKENDS, _PSA_SAML_BACKENDS +) class Registry(object): - """Singleton registry of third-party auth providers. - - Providers must subclass BaseProvider in order to be usable in the registry. """ + API for querying third-party auth ProviderConfig objects. - _CONFIGURED = False - _ENABLED = {} - + Providers must subclass ProviderConfig in order to be usable in the registry. + """ @classmethod - def _check_configured(cls): - """Ensures registry is configured.""" - if not cls._CONFIGURED: - raise RuntimeError('Registry not configured') - - @classmethod - def _get_all(cls): - """Gets all provider implementations loaded into the Python runtime.""" - # BaseProvider does so have __subclassess__. pylint: disable-msg=no-member - return {klass.NAME: klass for klass in BaseProvider.__subclasses__()} - - @classmethod - def _enable(cls, provider): - """Enables a single provider.""" - if provider.NAME in cls._ENABLED: - raise ValueError('Provider %s already enabled' % provider.NAME) - cls._ENABLED[provider.NAME] = provider - - @classmethod - def configure_once(cls, provider_names): - """Configures providers. - - Args: - provider_names: list of string. The providers to configure. - - Raises: - ValueError: if the registry has already been configured, or if any - of the passed provider_names does not have a corresponding - BaseProvider child implementation. - """ - if cls._CONFIGURED: - raise ValueError('Provider registry already configured') - - # Flip the bit eagerly -- configure() should not be re-callable if one - # _enable call fails. - cls._CONFIGURED = True - for name in provider_names: - all_providers = cls._get_all() - if name not in all_providers: - raise ValueError('No implementation found for provider ' + name) - cls._enable(all_providers.get(name)) + def _enabled_providers(cls): + """ Helper method to iterate over all providers """ + for backend_name in _PSA_OAUTH2_BACKENDS: + provider = OAuth2ProviderConfig.current(backend_name) + if provider.enabled: + yield provider + if SAMLConfiguration.is_enabled(): + idp_slugs = SAMLProviderConfig.key_values('idp_slug', flat=True) + for idp_slug in idp_slugs: + provider = SAMLProviderConfig.current(idp_slug) + if provider.enabled and provider.backend_name in _PSA_SAML_BACKENDS: + yield provider @classmethod def enabled(cls): """Returns list of enabled providers.""" - cls._check_configured() - return sorted(cls._ENABLED.values(), key=lambda provider: provider.NAME) + return sorted(cls._enabled_providers(), key=lambda provider: provider.name) @classmethod - def get(cls, provider_name): - """Gets provider named provider_name string if enabled, else None.""" - cls._check_configured() - return cls._ENABLED.get(provider_name) + def get(cls, provider_id): + """Gets provider by provider_id string if enabled, else None.""" + if '-' not in provider_id: # Check format - see models.py:ProviderConfig + raise ValueError("Invalid provider_id. Expect something like oa2-google") + try: + return next(provider for provider in cls._enabled_providers() if provider.provider_id == provider_id) + except StopIteration: + return None @classmethod - def get_by_backend_name(cls, backend_name): - """Gets provider (or None) by backend name. + def get_from_pipeline(cls, running_pipeline): + """Gets the provider that is being used for the specified pipeline (or None). Args: - backend_name: string. The python-social-auth - backends.base.BaseAuth.name (for example, 'google-oauth2') to - try and get a provider for. + running_pipeline: The python-social-auth pipeline being used to + authenticate a user. - Raises: - RuntimeError: if the registry has not been configured. + Returns: + An instance of ProviderConfig or None. """ - cls._check_configured() - for enabled in cls._ENABLED.values(): - if enabled.BACKEND_CLASS.name == backend_name: + for enabled in cls._enabled_providers(): + if enabled.is_active_for_pipeline(running_pipeline): return enabled @classmethod - def _reset(cls): - """Returns the registry to an unconfigured state; for tests only.""" - cls._CONFIGURED = False - cls._ENABLED = {} + def get_enabled_by_backend_name(cls, backend_name): + """Generator returning all enabled providers that use the specified + backend. + + Example: + >>> list(get_enabled_by_backend_name("tpa-saml")) + [, ] + + Args: + backend_name: The name of a python-social-auth backend used by + one or more providers. + + Yields: + Instances of ProviderConfig. + """ + if backend_name in _PSA_OAUTH2_BACKENDS: + provider = OAuth2ProviderConfig.current(backend_name) + if provider.enabled: + yield provider + elif backend_name in _PSA_SAML_BACKENDS and SAMLConfiguration.is_enabled(): + idp_names = SAMLProviderConfig.key_values('idp_slug', flat=True) + for idp_name in idp_names: + provider = SAMLProviderConfig.current(idp_name) + if provider.backend_name == backend_name and provider.enabled: + yield provider diff --git a/common/djangoapps/third_party_auth/saml.py b/common/djangoapps/third_party_auth/saml.py new file mode 100644 index 0000000000..db40104b14 --- /dev/null +++ b/common/djangoapps/third_party_auth/saml.py @@ -0,0 +1,49 @@ +""" +Slightly customized python-social-auth backend for SAML 2.0 support +""" +import logging +from social.backends.saml import SAMLAuth, OID_EDU_PERSON_ENTITLEMENT +from social.exceptions import AuthForbidden + +log = logging.getLogger(__name__) + + +class SAMLAuthBackend(SAMLAuth): # pylint: disable=abstract-method + """ + Customized version of SAMLAuth that gets the list of IdPs from third_party_auth's list of + enabled providers. + """ + name = "tpa-saml" + + def get_idp(self, idp_name): + """ Given the name of an IdP, get a SAMLIdentityProvider instance """ + from .models import SAMLProviderConfig + return SAMLProviderConfig.current(idp_name).get_config() + + def setting(self, name, default=None): + """ Get a setting, from SAMLConfiguration """ + if not hasattr(self, '_config'): + from .models import SAMLConfiguration + self._config = SAMLConfiguration.current() # pylint: disable=attribute-defined-outside-init + if not self._config.enabled: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("SAML Authentication is not enabled.") + try: + return self._config.get_setting(name) + except KeyError: + return self.strategy.setting(name, default) + + def _check_entitlements(self, idp, attributes): + """ + Check if we require the presence of any specific eduPersonEntitlement. + + raise AuthForbidden if the user should not be authenticated, or do nothing + to allow the login pipeline to continue. + """ + if "requiredEntitlements" in idp.conf: + entitlements = attributes.get(OID_EDU_PERSON_ENTITLEMENT, []) + for expected in idp.conf['requiredEntitlements']: + if expected not in entitlements: + log.warning( + "SAML user from IdP %s rejected due to missing eduPersonEntitlement %s", idp.name, expected) + raise AuthForbidden(self) diff --git a/common/djangoapps/third_party_auth/settings.py b/common/djangoapps/third_party_auth/settings.py index 12b362759c..a856aefa4f 100644 --- a/common/djangoapps/third_party_auth/settings.py +++ b/common/djangoapps/third_party_auth/settings.py @@ -1,51 +1,15 @@ """Settings for the third-party auth module. -Defers configuration of settings so we can inspect the provider registry and -create settings placeholders for only those values actually needed by a given -deployment. Required by Django; consequently, this file must not invoke the -Django armature. - The flow for settings registration is: The base settings file contains a boolean, ENABLE_THIRD_PARTY_AUTH, indicating -whether this module is enabled. Ancillary settings files (aws.py, dev.py) put -options in THIRD_PARTY_SETTINGS. startup.py probes the ENABLE_THIRD_PARTY_AUTH. +whether this module is enabled. startup.py probes the ENABLE_THIRD_PARTY_AUTH. If true, it: a) loads this module. - b) calls apply_settings(), passing in settings.THIRD_PARTY_AUTH. - THIRD_PARTY AUTH is a dict of the form - - 'THIRD_PARTY_AUTH': { - '': { - '': '', - [...] - }, - [...] - } - - If you are using a dev settings file, your settings dict starts at the - level of and is a map of provider name string to - settings dict. If you are using an auth.json file, it should contain a - THIRD_PARTY_AUTH entry as above. - c) apply_settings() builds a list of . These are the - enabled third party auth providers for the deployment. These are enabled - in provider.Registry, the canonical list of enabled providers. - d) then, it sets global, provider-independent settings. - e) then, it sets provider-specific settings. For each enabled provider, we - read its SETTINGS member. These are merged onto the Django settings - object. In most cases these are stubs and the real values are set from - THIRD_PARTY_AUTH. All values that are set from this dict must first be - initialized from SETTINGS. This allows us to validate the dict and - ensure that the values match expected configuration options on the - provider. - f) finally, the (key, value) pairs from the dict file are merged onto the - django settings object. + b) calls apply_settings(), passing in the Django settings """ -from . import provider - - _FIELDS_STORED_IN_SESSION = ['auth_entry', 'next'] _MIDDLEWARE_CLASSES = ( 'third_party_auth.middleware.ExceptionMiddleware', @@ -53,25 +17,7 @@ _MIDDLEWARE_CLASSES = ( _SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard' -def _merge_auth_info(django_settings, auth_info): - """Merge auth_info dict onto django_settings module.""" - enabled_provider_names = [] - to_merge = [] - - for provider_name, provider_dict in auth_info.items(): - enabled_provider_names.append(provider_name) - # Merge iff all settings have been intialized. - for key in provider_dict: - if key not in dir(django_settings): - raise ValueError('Auth setting %s not initialized' % key) - to_merge.append(provider_dict) - - for passed_validation in to_merge: - for key, value in passed_validation.iteritems(): - setattr(django_settings, key, value) - - -def _set_global_settings(django_settings): +def apply_settings(django_settings): """Set provider-independent settings.""" # Whitelisted URL query parameters retrained in the pipeline session. @@ -111,10 +57,13 @@ def _set_global_settings(django_settings): 'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.load_extra_data', 'social.pipeline.user.user_details', - 'third_party_auth.pipeline.set_logged_in_cookie', + 'third_party_auth.pipeline.set_logged_in_cookies', 'third_party_auth.pipeline.login_analytics', ) + # Required so that we can use unmodified PSA OAuth2 backends: + django_settings.SOCIAL_AUTH_STRATEGY = 'third_party_auth.strategy.ConfigurationModelStrategy' + # We let the user specify their email address during signup. django_settings.SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email'] @@ -136,30 +85,3 @@ def _set_global_settings(django_settings): 'social.apps.django_app.context_processors.backends', 'social.apps.django_app.context_processors.login_redirect', ) - - -def _set_provider_settings(django_settings, enabled_providers, auth_info): - """Sets provider-specific settings.""" - # Must prepend here so we get called first. - django_settings.AUTHENTICATION_BACKENDS = ( - tuple(enabled_provider.get_authentication_backend() for enabled_provider in enabled_providers) + - django_settings.AUTHENTICATION_BACKENDS) - - # Merge settings from provider classes, and configure all placeholders. - for enabled_provider in enabled_providers: - enabled_provider.merge_onto(django_settings) - - # Merge settings from .auth.json, overwriting placeholders. - _merge_auth_info(django_settings, auth_info) - - -def apply_settings(auth_info, django_settings): - """Applies settings from auth_info dict to django_settings module.""" - if django_settings.FEATURES.get('ENABLE_DUMMY_THIRD_PARTY_AUTH_PROVIDER'): - # The Dummy provider is handy for testing and development. - from .dummy import DummyProvider # pylint: disable=unused-variable - provider_names = auth_info.keys() - provider.Registry.configure_once(provider_names) - enabled_providers = provider.Registry.enabled() - _set_global_settings(django_settings) - _set_provider_settings(django_settings, enabled_providers, auth_info) diff --git a/common/djangoapps/third_party_auth/strategy.py b/common/djangoapps/third_party_auth/strategy.py new file mode 100644 index 0000000000..1d5134c6bd --- /dev/null +++ b/common/djangoapps/third_party_auth/strategy.py @@ -0,0 +1,34 @@ +""" +A custom Strategy for python-social-auth that allows us to fetch configuration from +ConfigurationModels rather than django.settings +""" +from .models import OAuth2ProviderConfig +from social.backends.oauth import BaseOAuth2 +from social.strategies.django_strategy import DjangoStrategy + + +class ConfigurationModelStrategy(DjangoStrategy): + """ + A DjangoStrategy customized to load settings from ConfigurationModels + for upstream python-social-auth backends that we cannot otherwise modify. + """ + def setting(self, name, default=None, backend=None): + """ + Load the setting from a ConfigurationModel if possible, or fall back to the normal + Django settings lookup. + + BaseOAuth2 subclasses will call this method for every setting they want to look up. + SAMLAuthBackend subclasses will call this method only after first checking if the + setting 'name' is configured via SAMLProviderConfig. + """ + if isinstance(backend, BaseOAuth2): + provider_config = OAuth2ProviderConfig.current(backend.name) + if not provider_config.enabled: + raise Exception("Can't fetch setting of a disabled backend/provider.") + try: + return provider_config.get_setting(name) + except KeyError: + pass + # At this point, we know 'name' is not set in a [OAuth2|SAML]ProviderConfig row. + # It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION': + return super(ConfigurationModelStrategy, self).setting(name, default, backend) diff --git a/common/djangoapps/third_party_auth/tasks.py b/common/djangoapps/third_party_auth/tasks.py new file mode 100644 index 0000000000..7466e113af --- /dev/null +++ b/common/djangoapps/third_party_auth/tasks.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +Code to manage fetching and storing the metadata of IdPs. +""" +#pylint: disable=no-member +from celery.task import task # pylint: disable=import-error,no-name-in-module +import datetime +import dateutil.parser +import logging +from lxml import etree +import requests +from onelogin.saml2.utils import OneLogin_Saml2_Utils +from third_party_auth.models import SAMLConfiguration, SAMLProviderConfig, SAMLProviderData + +log = logging.getLogger(__name__) + +SAML_XML_NS = 'urn:oasis:names:tc:SAML:2.0:metadata' # The SAML Metadata XML namespace + + +class MetadataParseError(Exception): + """ An error occurred while parsing the SAML metadata from an IdP """ + pass + + +@task(name='third_party_auth.fetch_saml_metadata') +def fetch_saml_metadata(): + """ + Fetch and store/update the metadata of all IdPs + + This task should be run on a daily basis. + It's OK to run this whether or not SAML is enabled. + + Return value: + tuple(num_changed, num_failed, num_total) + num_changed: Number of providers that are either new or whose metadata has changed + num_failed: Number of providers that could not be updated + num_total: Total number of providers whose metadata was fetched + """ + if not SAMLConfiguration.is_enabled(): + return (0, 0, 0) # Nothing to do until SAML is enabled. + + num_changed, num_failed = 0, 0 + + # First make a list of all the metadata XML URLs: + url_map = {} + for idp_slug in SAMLProviderConfig.key_values('idp_slug', flat=True): + config = SAMLProviderConfig.current(idp_slug) + if not config.enabled: + continue + url = config.metadata_source + if url not in url_map: + url_map[url] = [] + if config.entity_id not in url_map[url]: + url_map[url].append(config.entity_id) + # Now fetch the metadata: + for url, entity_ids in url_map.items(): + try: + log.info("Fetching %s", url) + if not url.lower().startswith('https'): + log.warning("This SAML metadata URL is not secure! It should use HTTPS. (%s)", url) + response = requests.get(url, verify=True) # May raise HTTPError or SSLError or ConnectionError + response.raise_for_status() # May raise an HTTPError + + try: + parser = etree.XMLParser(remove_comments=True) + xml = etree.fromstring(response.text, parser) + except etree.XMLSyntaxError: + raise + # TODO: Can use OneLogin_Saml2_Utils to validate signed XML if anyone is using that + + for entity_id in entity_ids: + log.info(u"Processing IdP with entityID %s", entity_id) + public_key, sso_url, expires_at = _parse_metadata_xml(xml, entity_id) + changed = _update_data(entity_id, public_key, sso_url, expires_at) + if changed: + log.info(u"→ Created new record for SAMLProviderData") + num_changed += 1 + else: + log.info(u"→ Updated existing SAMLProviderData. Nothing has changed.") + except Exception as err: # pylint: disable=broad-except + log.exception(err.message) + num_failed += 1 + return (num_changed, num_failed, len(url_map)) + + +def _parse_metadata_xml(xml, entity_id): + """ + Given an XML document containing SAML 2.0 metadata, parse it and return a tuple of + (public_key, sso_url, expires_at) for the specified entityID. + + Raises MetadataParseError if anything is wrong. + """ + if xml.tag == etree.QName(SAML_XML_NS, 'EntityDescriptor'): + entity_desc = xml + else: + if xml.tag != etree.QName(SAML_XML_NS, 'EntitiesDescriptor'): + raise MetadataParseError("Expected root element to be , not {}".format(xml.tag)) + entity_desc = xml.find( + ".//{}[@entityID='{}']".format(etree.QName(SAML_XML_NS, 'EntityDescriptor'), entity_id) + ) + if not entity_desc: + raise MetadataParseError("Can't find EntityDescriptor for entityID {}".format(entity_id)) + + expires_at = None + if "validUntil" in xml.attrib: + expires_at = dateutil.parser.parse(xml.attrib["validUntil"]) + if "cacheDuration" in xml.attrib: + cache_expires = OneLogin_Saml2_Utils.parse_duration(xml.attrib["cacheDuration"]) + if expires_at is None or cache_expires < expires_at: + expires_at = cache_expires + + sso_desc = entity_desc.find(etree.QName(SAML_XML_NS, "IDPSSODescriptor")) + if not sso_desc: + raise MetadataParseError("IDPSSODescriptor missing") + if 'urn:oasis:names:tc:SAML:2.0:protocol' not in sso_desc.get("protocolSupportEnumeration"): + raise MetadataParseError("This IdP does not support SAML 2.0") + + # Now we just need to get the public_key and sso_url + public_key = sso_desc.findtext("./{}//{}".format( + etree.QName(SAML_XML_NS, "KeyDescriptor"), "{http://www.w3.org/2000/09/xmldsig#}X509Certificate" + )) + if not public_key: + raise MetadataParseError("Public Key missing. Expected an ") + public_key = public_key.replace(" ", "") + binding_elements = sso_desc.iterfind("./{}".format(etree.QName(SAML_XML_NS, "SingleSignOnService"))) + sso_bindings = {element.get('Binding'): element.get('Location') for element in binding_elements} + try: + # The only binding supported by python-saml and python-social-auth is HTTP-Redirect: + sso_url = sso_bindings['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'] + except KeyError: + raise MetadataParseError("Unable to find SSO URL with HTTP-Redirect binding.") + return public_key, sso_url, expires_at + + +def _update_data(entity_id, public_key, sso_url, expires_at): + """ + Update/Create the SAMLProviderData for the given entity ID. + Return value: + False if nothing has changed and existing data's "fetched at" timestamp is just updated. + True if a new record was created. (Either this is a new provider or something changed.) + """ + data_obj = SAMLProviderData.current(entity_id) + fetched_at = datetime.datetime.now() + if data_obj and (data_obj.public_key == public_key and data_obj.sso_url == sso_url): + data_obj.expires_at = expires_at + data_obj.fetched_at = fetched_at + data_obj.save() + return False + else: + SAMLProviderData.objects.create( + entity_id=entity_id, + fetched_at=fetched_at, + expires_at=expires_at, + sso_url=sso_url, + public_key=public_key, + ) + return True diff --git a/common/djangoapps/third_party_auth/tests/data/saml_key.key b/common/djangoapps/third_party_auth/tests/data/saml_key.key new file mode 100644 index 0000000000..a6b7f7fa85 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/saml_key.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDM+Nf7IeRdIIgYUke6sR3n7osHVYXwH6pb+Ovq8j3hUoy8kzT9 +kJF0RB3h3Q2VJ3ZWiQtT94fZX2YYorVdoGVK2NWzjLwgpHUsgfeJq5pCjP0d2OQu +9Qvjg6YOtYP6PN3j7eK7pUcxQvIcaY9APDF57ua/zPsm3UzbjhRlJZQUewIDAQAB +AoGADWBsD/qdQaqe1x9/iOKINhuuPRNKw2n9nzT2iIW4nhzaDHB689VceL79SEE5 +4rMJmQomkBtGZVxBeHgd5/dQxNy3bC9lPN1uoMuzjQs7UMk+lvy0MoHfiJcuIxPX +RdyZTV9LKN8vq+ZpVykVu6pBdDlne4psPZeQ76ynxke/24ECQQD3NX7JeluZ64la +tC6b3VHzA4Hd1qTXDWtEekh2WaR2xuKzcLyOWhqPIWprylBqVc1m+FA/LRRWQ9y6 +vJMiXMk7AkEA1ELWj9DtZzk9BV1JxsDUUP0/IMAiYliVac3YrvQfys8APCY1xr9q +BAGurH4VWXuEnbx1yNXK89HqFI7kDrMtwQJAVTXtVAmHFZEosUk2X6d0He3xj8Py +4eXQObRk0daoaAC6F9weQnsweHGuOyVrfpvAx2OEVaJ2Rh3yMbPai5esDQJAS9Yh +gLqdx26M3bjJ3igQ82q3vkTHRCnwICA6la+FGFnC9LqWJg9HmmzbcqeNiy31YMHv +tzSjUV+jaXrwAkyEQQJAK/SCIVsWRhFe/ssr8hS//V+hZC4kvCv4b3NqzZK1x+Xm +7GaGMV0xEWN7shqVSRBU4O2vn/RWD6/6x3sHkU57qg== +-----END RSA PRIVATE KEY----- diff --git a/common/djangoapps/third_party_auth/tests/data/saml_key.pub b/common/djangoapps/third_party_auth/tests/data/saml_key.pub new file mode 100644 index 0000000000..e93f6dd59b --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/saml_key.pub @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsDCCAhmgAwIBAgIJAJrENr8EPgpcMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTUwNjEzMDEwNTE0WhcNMjUwNjEyMDEwNTE0WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDM+Nf7IeRdIIgYUke6sR3n7osHVYXwH6pb+Ovq8j3hUoy8kzT9kJF0RB3h3Q2V +J3ZWiQtT94fZX2YYorVdoGVK2NWzjLwgpHUsgfeJq5pCjP0d2OQu9Qvjg6YOtYP6 +PN3j7eK7pUcxQvIcaY9APDF57ua/zPsm3UzbjhRlJZQUewIDAQABo4GnMIGkMB0G +A1UdDgQWBBTjOyPvAuej5q4C80jlFrQmOlszmzB1BgNVHSMEbjBsgBTjOyPvAuej +5q4C80jlFrQmOlszm6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrENr8E +PgpcMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAV5w0SxjUTFWfL3ZG +6sgA0gKf8aV8w3AlihLt9tKCRgrK4sBK9xmfwp/fnbdxkHU58iozI894HqmrRzCi +aRLWmy3W8640E/XCa6P+i8ET7RksgNJ5cD9WtISHkGc2dnW76+2nv8d24JKeIx2w +oJAtspMywzr0SoxDIJr42N6Kvjk= +-----END CERTIFICATE----- diff --git a/common/djangoapps/third_party_auth/tests/data/saml_key_alt.key b/common/djangoapps/third_party_auth/tests/data/saml_key_alt.key new file mode 100644 index 0000000000..d54d58a3b6 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/saml_key_alt.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMoR8CP+HlvsPRwi +VCCuWxZOdNjYa4Qre3JEWPkqlUwpci1XGTBqH7DK9b2hmBXMjYoDKOnF5pL7Y453 +3JSJ2+AG7D4AJGSotA3boKF18EDgeMzAWjAhDVhTprGz+/1G+W0R4SSyY5QGyBhL +Z36xF2w5HyeiqN/Iiq3QKGl2CFORAgMBAAECgYEAwH2CAudqSCqstAZHmbI99uva +B09ybD93owxUrVbRTfIVX/eeeS4+7g0JNxGebPWkxxnneXoaAV4UIn0v1RfWKMs3 +QGiBsOSup1DWWwkBfvQ1hNlJfVCqgZH1QVbhPpw9M9gxhLZQaSZoI/qY/8n/54L0 +zU4S6VYBH6hnkgZZmiECQQDpYUS8HgnkMUX/qcDNBJT23qHewHsZOe6uqC+7+YxQ +xKT8iCxybDbZU7hmZ1Av8Ns4iF7EvZ0faFM8Ls76wFX1AkEA3afLUMLHfTx40XwO +oU7GWrYFyLNCc3/7JeWi6ZKzwzQqiGvFderRf/QGQsCtpLQ8VoLz/knF9TkQdSh6 +yuIprQJATfcmxUmruEYVwnFtbZBoS4jYvtfCyAyohkS9naiijaEEFTFQ1/D66eOk +KOG+0iU+t0YnksZdpU5u8B4bG34BuQJAXv6FhTQk+MhM40KupnUzTzcJXY1t4kAs +K36yBjZoMjWOMO83LiUX6iVz9XHMOXVBEraGySlm3IS7R+q0TXUF9QJAQ69wautf +8q1OQiLcg5WTFmSFBEXqAvVwX6FcDSxor9UnI0iHwyKBss3a2IXY9LoTPTjR5SHh +GDq2lXmP+kmbnQ== +-----END PRIVATE KEY----- diff --git a/common/djangoapps/third_party_auth/tests/data/saml_key_alt.pub b/common/djangoapps/third_party_auth/tests/data/saml_key_alt.pub new file mode 100644 index 0000000000..1221357e6d --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/saml_key_alt.pub @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWDCCAcGgAwIBAgIJAMlM2wrOvplkMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTUwNjEzMDEyMTAwWhcNMjUwNjEyMDEyMTAwWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDKEfAj/h5b7D0cIlQgrlsWTnTY2GuEK3tyRFj5KpVMKXItVxkwah+wyvW9oZgV +zI2KAyjpxeaS+2OOd9yUidvgBuw+ACRkqLQN26ChdfBA4HjMwFowIQ1YU6axs/v9 +RvltEeEksmOUBsgYS2d+sRdsOR8noqjfyIqt0ChpdghTkQIDAQABo1AwTjAdBgNV +HQ4EFgQUU0TNPc1yGas/W4HJl/Hgtrmdu6MwHwYDVR0jBBgwFoAUU0TNPc1yGas/ +W4HJl/Hgtrmdu6MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQCE4BqJ +v2s99DS16NbZtR7tpqXDxiDaCg59VtgcHQwxN4qXcixZi5N4yRvzjYschAQN5tQ6 +bofXdIK3tJY9Ynm0KPO+5l0RCv7CkhNgftTww0bWC91xaHJ/y66AqONuLpaP6s43 +SZYG2D6ric57ZY4kQ6ZlUv854TPzmvapnGG7Hw== +-----END CERTIFICATE----- diff --git a/common/djangoapps/third_party_auth/tests/data/testshib_metadata.xml b/common/djangoapps/third_party_auth/tests/data/testshib_metadata.xml new file mode 100644 index 0000000000..e78b2e1733 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/testshib_metadata.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + testshib.org + + TestShib Test IdP + TestShib IdP. Use this as a source of attributes + for your test SP. + https://www.testshib.org/testshibtwo.jpg + + + + + + + + MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD + VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4 + MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI + EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl + c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C + yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe + 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT + NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614 + kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH + gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G + A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86 + 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl + bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo + aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL + I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo + 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4 + /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj + Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr + 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA== + + + + + + + + + + + + + + + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + + + + + + + + + + + + MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV + MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD + VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4 + MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI + EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl + c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C + yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe + 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT + NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614 + kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH + gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G + A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86 + 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl + bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo + aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN + BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL + I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo + 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4 + /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj + Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr + 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA== + + + + + + + + + + + + + + + + urn:mace:shibboleth:1.0:nameIdentifier + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + TestShib Two Identity Provider + TestShib Two + http://www.testshib.org/testshib-two/ + + + Nate + Klingenstein + ndk@internet2.edu + + + + diff --git a/common/djangoapps/third_party_auth/tests/data/testshib_response.txt b/common/djangoapps/third_party_auth/tests/data/testshib_response.txt new file mode 100644 index 0000000000..74def7401d --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/testshib_response.txt @@ -0,0 +1 @@ +RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2V4YW1wbGUubm9uZS9hdXRoL2NvbXBsZXRlL3RwYS1zYW1sLyIgSUQ9Il9hMDdmZDlhMDg0ODM3M2U1NTMyMGRjMzQyNDk0ZWY1ZCIgSW5SZXNwb25zZVRvPSJURVNUSUQiIElzc3VlSW5zdGFudD0iMjAxNS0wNi0xNVQwMDowNzoxNS4xODhaIiBWZXJzaW9uPSIyLjAiPjxzYW1sMjpJc3N1ZXIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwczovL2lkcC50ZXN0c2hpYi5vcmcvaWRwL3NoaWJib2xldGg8L3NhbWwyOklzc3Vlcj48c2FtbDJwOlN0YXR1cz48c2FtbDJwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbDJwOlN0YXR1cz48c2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgSWQ9Il9kYzc3ODI3YmY1ZGMzYjZmNGQzNjkzZWUzMTU2YmE1MiIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIvPjxkczpLZXlJbmZvIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXkgSWQ9Il85NzhhN2I2NDE5YTMxOGQ4NmUzMzE0Y2Y5YjFjOTEzZiIgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLW9hZXAtbWdmMXAiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIiB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIvPjwveGVuYzpFbmNyeXB0aW9uTWV0aG9kPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNzRENDQWhtZ0F3SUJBZ0lKQUpyRU5yOEVQZ3BjTUEwR0NTcUdTSWIzRFFFQkJRVUFNRVV4Q3pBSkJnTlZCQVlUQWtGVk1STXcKRVFZRFZRUUlFd3BUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLRXhoSmJuUmxjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3SGhjTgpNVFV3TmpFek1ERXdOVEUwV2hjTk1qVXdOakV5TURFd05URTBXakJGTVFzd0NRWURWUVFHRXdKQlZURVRNQkVHQTFVRUNCTUtVMjl0ClpTMVRkR0YwWlRFaE1COEdBMVVFQ2hNWVNXNTBaWEp1WlhRZ1YybGtaMmwwY3lCUWRIa2dUSFJrTUlHZk1BMEdDU3FHU0liM0RRRUIKQVFVQUE0R05BRENCaVFLQmdRRE0rTmY3SWVSZElJZ1lVa2U2c1Izbjdvc0hWWVh3SDZwYitPdnE4ajNoVW95OGt6VDlrSkYwUkIzaAozUTJWSjNaV2lRdFQ5NGZaWDJZWW9yVmRvR1ZLMk5XempMd2dwSFVzZ2ZlSnE1cENqUDBkMk9RdTlRdmpnNllPdFlQNlBOM2o3ZUs3CnBVY3hRdkljYVk5QVBERjU3dWEvelBzbTNVemJqaFJsSlpRVWV3SURBUUFCbzRHbk1JR2tNQjBHQTFVZERnUVdCQlRqT3lQdkF1ZWoKNXE0QzgwamxGclFtT2xzem16QjFCZ05WSFNNRWJqQnNnQlRqT3lQdkF1ZWo1cTRDODBqbEZyUW1PbHN6bTZGSnBFY3dSVEVMTUFrRwpBMVVFQmhNQ1FWVXhFekFSQmdOVkJBZ1RDbE52YldVdFUzUmhkR1V4SVRBZkJnTlZCQW9UR0VsdWRHVnlibVYwSUZkcFpHZHBkSE1nClVIUjVJRXgwWklJSkFKckVOcjhFUGdwY01Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFRkJRQURnWUVBVjV3MFN4alUKVEZXZkwzWkc2c2dBMGdLZjhhVjh3M0FsaWhMdDl0S0NSZ3JLNHNCSzl4bWZ3cC9mbmJkeGtIVTU4aW96STg5NEhxbXJSekNpYVJMVwpteTNXODY0MEUvWENhNlAraThFVDdSa3NnTko1Y0Q5V3RJU0hrR2MyZG5XNzYrMm52OGQyNEpLZUl4MndvSkF0c3BNeXd6cjBTb3hECklKcjQyTjZLdmprPTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE%2BPC9kczpLZXlJbmZvPjx4ZW5jOkNpcGhlckRhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpDaXBoZXJWYWx1ZT5sWEEvSGI2SlIxaW1UM2M1citrQU9taHVieVYvOUpqTUNzdkRJYlBEckxVR1g0aWFVbGl6c2d0dkdzRzdYOVpQWUxhc281U2ZlK1dTbVpKeW9tMGc0UU9HOWd6R3FIVGwybzFGMlJib0ZKS2FzaDZoQ011c2dSRmpJWElSUzdvTWJJTGxmcGhvcUN2c0pGdUpKY1FldU9SeWwyZmlrcUJSclhjNmwyMks2YzA9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk%2BPC9kczpLZXlJbmZvPjx4ZW5jOkNpcGhlckRhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIj48eGVuYzpDaXBoZXJWYWx1ZT5UQWdqL1d3ait2b0ppdHNhUE9IdmFSUHN3WE5oaHhEMmx2Q0t6MXJzQmtYbmJheVhlNmJucE1CMHg5aWpFMVdqOFV4YmtJQmhBMTEwRHhhTjZabkhReUs4amI3U29VU09jQkovUGxrT1NJc21zcS94dTVZTDJrdC9qdTdwRDR6K3BpcUlJbWRSVXdkcEhPdXFSVUxzUUJNU1NCRU5QT2sxQkRjcWZxb091N1g1VFIyOHE2eEwwUlFoV2NqMHBoTW9BMHdZS2ZFU0tacmRDQVRLcXVFcUJTRGJzRThNekh1cndiUkVkcjM5YnpBMGRNTm1zVFBFWmpsdkdqSEJmdVRaS3VPWXBoR3lFV1FVa29PbEtMQ1ROQ3VQYnhaWjFZNi80SGhlTGpQL3pnSU9ieWdLVkdadmNBSWpyTmZXNi9iQkJ0MVJjbndKV3pTZU5XR3hnd2hnZTJDank4RVFTUHJxbWZYK29VSVY3NExmT1NhV2JONXRwK3dtbzgveGlzWFErRysyZzA1ZnBiZEVmcEhacFZhMmFZY24xYWRtYy9uK3p1U0w2LzRTVWtXcTIvRHpRTTBWQVNQN2o0MXNIc0wwZEt2ZkhiQk5BcFB3NzdyTWdXRkwxcjVRajZiUmVjSElkQ3ozamZLcDZtdzBURzFQaUxCc2FXZ2wzYnZVR0lJNmxOWTg1TStFWHpoUnNCY29uVjgwUWFnKzBoTks3OWNteFkvOFByOGZkdXErMmgva3UxUzN2REo0cTlpR0FPaG1MUUFrWEQwWGF5Q1hXa0x1MEFyTmdOalNjaU55UXY2c3RmOUx1NGdUd1lKWUsyV0lWYUEvTzdMbUl2WmxkL0thL2VSaHdJQnV5YnpmemxWSVA0a2VVVmpldDVZMnZUVzA5bG02bWQzV1dmOTYrb21yNGVBQjllWjBmaFRxRkV4UzJkQVFqWi9JV21ZNnNXQXJCSkg4aEhya0IxdVBrQkZYTVFNbSs5T3ZKMWpnWXZtdVJUcHZzc1ZlL1MwWDhIVlVSdUpZVDVBY09YNHhuTWw0QkpXdnlLbzFFUms4bmRZOXJTTkJscGUrSXRUQXY5ZDgveVdJcGlYRVRLWGpKbTlVVWt6NnIyclhDUkN0VDhWa3J2M29jZmY0Z09nUjlTSWR0b21hU2w5RG9sTjVFL0ljTm1DUkhrbTR0U1J4bDNrNzNiMy8rNUxJNDhHc3l2c1VsNlNaZ1dCNmhSdDZaNDNuS2Iza3czOWd1cjk0VStrcURuUndub3lyeDVoc3N1NWRVVFNURkJPajhENGk0SjZwYi9sQ3FvWDVtR3lNODVJWnpxQk1Sa3IzeldPUzg3SVhBQm1uVDZ6aE1NdEpzN3EwRG9WZUhQbExUeUlVRHFMZWlaMjREQzN3Z1BjNmh0STVNeU1EZG5OM0hLczdnT3lhY3ppV280c3I1RGJCc3FqaFd1ZzJIanJEQ2hXZUszY1NuTlcwbkZCT1RJOFZMSFJXK2lhd3ZJWnUwZGdSenR1aEFjQ3lCelB0SHUrSzZlZmgyR0lNT0NhVVJCUzA3eGRETkw2MG9jOTBZZUlmOGV5WDFqV3ZDaDJZSnNxMDRtU3AySUFFQzFQSS9mc3lleGIrU2lMWTNzR1FyNEQ1bGx6ZHh6VEpKa2tYd2c4dHMxeklvSEx2TDdkQ29BRVNmNDJVVERFdWo4bGtadjYrM0JkNkwrM3Z6WFFPb2xwQ1kxeGRodVFOQjhsbG5KWHRNMmdtb1NEcmRwN1JzZWtTM2hkOWkxSkxkYWR1bW9oM0hmd0x6d0c4SkpCRkpmb3k0cGtzM05vMWFyT0lKRVBzZ3ZCMzZZUitScFFvc2hHM01PS0EwTUphcDNJbE5yQlVjdmw4WW5jSGkzbE9CUWpySFcrSURVVHhxZG9jdmw0NTBscmVUdkVoZXYraUFLU0J4alhNclNDQytaOWFIS1BJR2s4UzR2dWtudURIMUpsdTNLSFBwNFA5bnhSMnp3V3ZldktkYjNLdjZ3emhidmlxR0pPY05CeUZzRVJvNkEwSVVNVHQvOFJkQzJkZndyTjNBTHZ4dktrTmE0c3dEcElqYkRkb3RrZWgzT29FTVRETGE4M1R6Ym9ROFdWcDJLbEJiZDlWdXVNRm8xVUtrN2Q0dFh4VXRnWkZ3YlZQUWNzU21TV1dud1QvMFhxdDB0Yk8zQ0lnNkVZOHFXaUxoWU5naGw4eUhXTGIxbUUzVWNoSUhhVDBoWmNXTXVLMmtNbVpNMlJJeWVJY3o4M2NmY2lSWFJUb0g2TzFPenF5ZytBVUZiendibXQxR2d3V1hnMTdZQVJxSFViaDZwdktUMGJFRzdPUDRMZ0U4ajQvdTJoQjFraUFnUXpQSC84RHVCQlQwdkdOeElnKzhNWi9ObUFmUWRxODBlcVNQTDVpczlGbkZVei9GUzYzdEVCb0xYZnk5VzFRUjBsZ3VUY3o5b3l1dkdPdmVDZUt4RE9pZU9ZSktFTTlhVFVzVjRGc2c5M0NzMTQvRkhXQzJpclRKN1J3Ymg4eG1WUzBqUnVFQUdBejdETDNaSkFiRElLUUM5ZWZ2QUYxRHREVjZiVEI5cnNlczdsOWlDS3RnSnRWRE83Nmc5M2tWSGwvdXpYbVhwQ0NveS9XYStvVFErek5WL0lMbXAwVnorRnhJRHROUmcyL0V3eENyN042RW05ZmRibHgrMjYyWkdEVWVKcXcwNjB3L0RDTThiMDJmc2dyQTJ4NUVvcjF4a0tmcXMvaFMvVlo5azlVRHB0ZVFaMWdrSEU5TEN6TzY1azFRRTNENDNRK21ReFM2cGRJL1BpOUUxVkRoN3pMemdpOUVscXFLVHZNYjhPQUxmdSthTStPWlFaUjN4L21UVjQycTNmQlZya2lYRHpNd0pkclhjNW5UcmxLSDJTSUZsV2JaUzNXK0tHUVpoVzJ4dVRzdU9yS1FiVDY2OEVRbEpNTEhuckxLQTFrV0NvalJKV1hqZGFxSGVrWURDWjlFcDZaRnZ4NjJzWlRTdWMrVlNjK3UxSnZjT0w3NTZzNEFFWnNjR3ZnbXNxbUx5MldYOHpLbGw2MEdVUlE0YlBHMHd3YVUxRGF3dnJTejZqTCtyUjVBTTlGdTFlQi9WaUZSVVo4R0prR0VIOTdRWmNKcjV6RFpXNnZkOVBZRnlTd0Zqa21rOWxuZ1NFdFNPZFFITmZTZXRxbXZrcS9HbTB2enlTSWVyM3N5OCszR2IzNjJHaHd5MFNCekp6dEdHM0dQWTE3NUxRK3FWcGJCc1Y0MmgvOExPcnFaUGpVY0RrUk1NMU1LTHBtOEpSeVRGeWM5c0NxZ1lVMzNBbTByRzlkTHgxeVVKaTkwUncxc1pDa0lYUFIxYWFRUlkyNFEyekhVNjNBNHZDQVVaaVBOSHdVZlI4Qzk0cDdDeks5UTlreFpoaGZ6bjRBOFdadVZMSjkvWWI3d1RmNWQvNlNmNVFXWUM2anAvbUVWMDAxRnVBZnUzcUZNNmNuNXpXV2xkR2tjaU5RcUJ3SmZoWk9oUnk1VjBEaW5rMDBjSVlncDFmVnVnWHFkR1grV1ZCQkJ5M29va0toYW05S2RPc1N6aEI4NXZyN3h6R2JsREVXVGhFN0F5U3duRUVVNnBjcXpxR1E3Mk9KLytWS2I4ZVNPVWxzQW1LZnZ0czgzTHBYR2o0dkdRR1UwNVptK2grakdWeEpjMTJSQW5lbUhYK1FiNVhJdGk4ek1CazJkT2I4NUVPRUlvVnduWXpmSmhqQmtpOFhYMUtWaTVWbDE5dmV4OExxQ2pLdW9JeUsrSFpVQWtGMmpqY01WenUybXEvM3JPblJvTUhqVEszbFpGZm53S1E3WUxqd2dlVk45QnBmNm1Zem5Bb1RhVG1kQTUza29ocnMrVExuK0toUEpCRFc3Rml6L0ZDbVhzU2dJQ2tQcHAzVnJnQkY0N1ZDUEtPQi9yR2hPaklKd0V2bjgvZ0o1MU5qSmY3NkI4OUxHKzhLOXZpV2ZCeDRvMGxIczZLRmtCSk5RSTF4TCtVRGREWThURitlNXFtaVg3TTh6QmVsQjJlalZKaW9DRkhUcG9mRlZyT0kzTTlGcUk0Mk9KYVdrQytFTFJCaHR6dmJxaisrMWNOdlArcXVKQkRseTZNY2d3SE5BbzlhOHZIcjNjTmRHdmdjVGNFemx1aHpXOW9wb0dSTERPbHRUT0RqOUNQeUVXU2VablFxV2pHRGZiZkRkWm85bTRTWmUxTjQzNUNZYzJBY2VtR3JDdjAySUhyNmgvR1dZMTRFRlJ6T3crTFQ0Skg2TDBzM2w5V1JlZVlvR2NJc2RxYmhrN1Y0OXF5b1lBKzRlb3IvUi9VRzhZaFFYVzJJckQxaTVveDRGTEtXa0Q1UWtKRWU0VmpyNUVRTTBNTHJzNnE4YW5URklITGV3YXE4V0lnanJLS2FtRWloU2tGK2RnMHdScllTeUpuSzdySUsxWi9GQTlPakFUeGlIN0Y1TWcraGhlbXozYlFrU1FTaGN2T1lVSEdjU2sreHQrMXBuSG1lc1ZZTUlCb2d4S3JkUy9yLy9LM0lxdWR1Wko1bE5oVElXZ3dISkpiTVBHTG9mQUJybTlwZUFCVG5mVUFkcDIxK08wQmluVjdYZVg4RXFmVGhVejhrY2Z0MmgrSW5hajZwd1lDdGJ2MmE4dld5UXFKM0haNnBiZHV3bUJFVzhMa3Y2Qm44Yng5TllZMTdyMDliWDNCbmF6QWpGVW1EN3l6R3pLSnR2c1ZVOUw0RmMyU1B4ZUpQVjluQy91c0ZKc1ZlTEFMMTdqVERyV1k3NXhRdkpDVkJGNFlIS0JHaiszSk5WSWRudElOam5DTWhRak1CZU02V29RRkcyei9IQU9hU1lnaXlJSnlaNzd4MW1UYUtuTkNvSTlzZ3JNZkJzUE9mTXJUbkVGbTR5SThERjFGSTh4VnVONnpBcWp2dGkzNVczRGIzdG9Wa0pYVWk0OStKZzIxRWwxSkNqcjJoVmhBQjN5dE5kV2VnTXlTSFRnY0tRVVdRQmt3WUlEbUphdjY5am9udE9RdWpVbGFIM0lBRExHMUpPSWxuREduN0F2OUhkb0JzdnA3MDdLN20xZmJwOUxZK3NCdkwzcXdNbmpZbGhuZHBmYjdVbGxEajl2SCtuNDE5Z0FWMU9GUkRmVHVkVkRpdEFlQzI5ZWRjSmFGZXBYbmpKeHpvTzNqZHFrVTBiMWRmeEo2T1BCa01XSzJKcDZqTmllVlF6emwzRGJWMnRjcTNpekhQVmRySVZ2eEFqVWl3eENWK1VLTzZmMmlXaG9jQjhsWWE5U2xPOTRxd1Y2SkxSbDlIU3pFbDZtQUdRKzRCaW90aEhleDd2ODlGYnJ3eW00UjkwOGl4cU5odzNCc25wcHkyVzhlQXJtcENxMTRHdjlpM3R4em1mS1c3allIV2xWT0JQZFdoSnQ5NTZWbmliV2pWaVBBME9WOVNRWFZ6L2tpSit1WnZzT0FPY1h0YVRDaTZQL0dDMHJyRmhLc1paQW82ZE5paUF0N1BtVzduQjc5RDU5SzRBd3RNaW5iV016TjRQQzFGcHA5eklQTlEvU0laY1IwN0FjMnJ0ODdoQ0JPNUNIY0xhL2EwZDcxZDNZenEzNFlSWDZRYmdRY2taVytLN0FTbGpUcnFQczBHUXo3eFVRVjY1SnRBUTZvbjBxWGwwQ3drdUFCR0gvVVZ3TXpTalBuSnh5WmhQczN5NFhmdTVOUWhabkQvWWNtenR2Y0tkanBvMExSZzkxZnN3QjQybS8vL282VTd5K1hJNGlXMDMxRFQ0R3ViVWV4NW5mZTByWjdlRTRMaGJkaWJiWVRkTDFESHJuTGhYUVpDaEFRSno5SVV1OWYweEtMY3ovR0lubzRQRC9VK2hKOVkrb3FtelpGM3NhVXpKNTRJY1lGeEVROGo5L25nOTVpWHc0SWM5aXovZmY0Wm9hN2hJMTUzalBMNHFOcS91akphYXd2MXpxRlFhRVJYZUU1UyswZTMwaC9UKzByMmMxMTJkYndZdXljN2UwNi9RVnJpckdSRnQyZnRHUFN2VVRMdTJKVzRBUHVDN1NpNmpYWjQ1QlArenJTNzJteWdQQm9LTSt6N2RYTWMySmhQTnhQcmlGRVJlUmJnQlFFM1RSckpMSVpaR3NPczNJbFdBdGpLRTJaVFd0bjNJQmFQM0loY2sySFJ4MnRXRDJYRnYweTQ1bXhlQnkwMXY0cjAwQjJnMW9JVlYvZkgvajZUQnFLU2VENjBWRGZ3OXMxeXU1VUVhbzRicy9oZmFjWVpZZzg1Y2daS1QrTkZCeHBadG84M3E3bzRBeUNMQTl5dzQ2ZFRzdUlSYnBsMC81MVp0R0hDa29YMkdOa0JRc2pFWTVSNHVoZGRJNnBFSkxuaWNvVnpGS0dsTFErZDJMKy9odnBldHUxMGFTVHhEMmJqWlMrUVlHM0VLZ0VvbDZveThVTHl4bjRMb041bk1zV2d3N0p6NDJWT0V1ZHFENHY0ZVFUdXB0NkRpQzhvRzVzaVdWZER2amZpUHdwN1l5cEFZOE8reFBLOVgzWkU2bVR6aEV6TGxud0Jtc1RuQWpjMTRsYTVha1FBdHJRaXZIY1Vmb1pCcWtQKzFqTmdOZ0lYQlB3WlRBMHh5aEVuYlRMK3JPd3dzcUEycjdMbUlTaFpNa3V0cUQzdC9GbmNBTCtkaTJvQ2pBOVRNdnVwMnRqNm5FaXNwbXpYcmE1WWVnUlVjOE5UNjJxL1dXU25aKy9pb0hUMWFjeldJWG9sN1JhZ0VlQlRlMkFlVVEvam5wd2RwR3gzUldLeGIrajdtN0RuaVpoSmlzZUw5a2JleC9RQUFBV1VxajRBQldtdEptQ1QrUy9KVit6R2FGaU4ybXFzcUd0Mm9qN2w1UzRmMkozcXdxaEpNbXVDRGlteUpzQ0FzOVlNQVFmOXRFRVNSa3pKTy9wOHdEamlFbGRZRUdMNkl2RHJkWHFERzhSTGJkQnczTHJxeUJQOEYwU3lzWmlHYWdXY3BSZGY4NmJKdXgra2gxOXo4eldvSWM4OUVBK2JCVm9ON2M5TEFETEFPbzlYY0pqdllJcFRiWXlJTk1iOVpCRGZlb3d5ZkViZ2Q2bGxiK004MzB5SXFIWVEvaFl0dWpWaDhXazdseG1mMjJzL280eUp3aHNYQlV2OWVWQkFyTDVmbUxPM3NjRmdBejRsR2cvbEN2cThSS0JRaFFYd29CNTM2aDdkRWFnWGxqQk41WGhZcTVmNkZhK0xRRkFIRnlyM2VHT3RZNjJsU3NMUE9Sa2VGQXl6RTY3bk1BZ3lrb1hEUHRrUE9hd2x6bW4zdEVOT3FMMDVRWmEzTkFvZ1RtYVZLYmlUTk1RVVF5M2JQNElYdDE0RlNEa3pweU5pREptcE5yeDJ3cjlVNmJvT08xQTl5eHFMZ1ZHdVNXc1E5Y1ByTm4zV2VZVXJvTjhhZkk4b2w2bFNuODgxaEUrU2I2OU9lZ2UxY09RYUduTUJ4WlpiVThzVDJxd0JsVWFzc0dYWlBOMUU5b3M3UUh5bzJKcmxkU0xrWGpsWWpxV3I2SkJtYmxiRXpRWXg4clA0TDVDUG5RME96WjA0MDV6MklUZXBzSUpHMW90Wng4VmIyWGwvSDNqajNja2F0TUZLU1ZrNTdBQkVIRUp3a3pmbXB4Y3Z0SzBjK081MGpPZUpTOGpJRUhnZUJJSkJTNzRRb0F2VGcvbW5NeTVHVTRGYllHSHZHMDBzYXdBbkNVbW90cm13R2dzcGhkM2orNFNuYm9HZ2Mvd0NzN2FTM0NJcHBBbC8wNTFCbWZhYk8rV2JpRDl4c3dzaElxSjZua3RMSk9mWmM0NjZVZmVpTkVJeG04cU5yQncxTlBrc1lXOWtjK2FtM3diTk1PMVp0N3hIYXU1M01odDVFRExIUjZ6a0wwcGFjY0ZuSndCNXFsS1hSN01Lb3huSmo5VTQyS1FOTFRQditUL2NlNG5oS2dIc2dua00wbU1YelF6Tkx0MDBDdkdGWDRuNnE4Q1JCRnY2SHFSVVdwWGFGdWlrc3Mwa1M3RDc0TThTQ2VnTmRuWit4Z3BLQ0IzOWZWWDEvYmMwUjlDbWN4M050d0hFYXNkZllpUlRIcFI0bGJndG1RUUcrYWNTdVhJWVRaSWFTRzlLWlZVZ3oxeFI4TVhMQXAveDRHbGMzaGNCMURnZ0cybE82RnNBTUhBTDExU3NySk1RUmZsUmxJOU0zNFB0SHRTY2pqSGcwcHRMT3JCOUI4c0NqMEk5bG1aWHU4cW1pMEQzMmg2VC96MnRvOFJMcmExVldZblp5NW9nYmhiQ2E4Sk5JZmNGQmJDTytpUXZlN2xGQ0RXZGJncDZJallGcXR4amdGUmkzVURaMExtQjVqSk9lejA1VWRNT3c3SjFudkhvSDM3RGZFclE5VWZKeUlpRjdGNzR4c2ZMSFIxSXpGenB6YnIyM2QwU3c5OXlCNjlDa1ZtdERCaXo3aWFmaVpXYzBZU2svajkrWDR4NENwU1diOVRMRTY5djI1MStjS0xzZzRPT2ppUHVSYTRnL25oaFB6eGN5bGE4WjNYb2s1ZTdJRy9BbWtCNkEvL29pRFdEKzBzbXJGOUI4VlNnVENiRHRNdFNyUStGUVBzZHBMdG52UndOY0pGcHJEaVVHN25FUTdMQnhoZHhraTZ3dXRuTnY1b2dsUkdheHV4Tk9XYXZaNGUxOTVEaXlVQkN4cjM5MlRjUkxKNExIOE1rTUpnNml2dmlNYllqeTBwSVpLVGJGSjRzOXZ6WlFwVjZVd1FlWEM0TGRWd1E2dlltb05Mb2JCaUlsSE9hUUUxM1hpTEliVTU2aEJVRnJqSHhiNzNRUm1SQ1poeWZpVUNSdUg4YWZjOEpyMFFkMXlJYTJNczd4bU9FczlCWmFwejdTR01OeURXUXlIY0Q5VzgvQ0d1OFlhUldRaWlBQTU4MDRlQWF6MmJnYUNuR0wrNHdZeXpYOXNvWnVGWFZ2eklDbVk3bVF5c1pVMllqUVZoRjdHZWFGTDNKL2ZGTTdqOVdiaThjZG1MNzJENGdLVDVXUmpKTGNVY01HNHNXZTJoNkJncWVybDNJeDFmMzRteGZOSUpFVnlLTTVzVW54a0kvTTFORDU5M2g0UWJGYWVUTjZkc2NLenRrK084OW82Rnhma1dvUFZYRlZJTmF0WFhoSVJHU0cvKzhSQXNNYi81QUZMb0Y5U2x6YytXaWkxQkQ5RGhwZFQvd01ya0lDUWdzVklMbUt4bnhobXZhS3pvcGEyaG9GdXBiL3A2Z2hMQVZpY2RROHZJa3Y4U0hBeGZJc0sxaFZXcHNRMUl0SGw5N3lnSHNtQ0lkR1NJcVNiaGwzdW9CNXhWWWVGV0FObXpYQ3g0OE5oTlc2SnNHZ1c5RXp1NjdlY2Z3VUZlUEZpYXpvbWFPUTFRc3ZjYVhHVGIxWjA0UzdQay9OZmlCWWFJSkoxbGlIMnBaeDhMZGZwZTVuRENtbTlYTVZHbUY2SVdWVjM0QW1uK1B5TlNCcWxzZDZyQlNjWVRISm43ZElUZm9Zd3JvZjZvNVBUR2lwUHNlYXNHbDJoWFpCMWRYc1U0aTJKVDNLenVTeUl1RVU2Y2Z5M242T3duNmNjV0NyUFNLc3Vwb2Z0QithenZpZzd0bUFvRHlFTHN4eUE1dzQrZ0RmQm1BeStwNG02NnVNWjFUWEJiSTZhZ2RzVEpteUtHMm1NNFZrUmo0Z1cvcW0yMDNISno0YlVJN2dlcXNHQktNbGdPR1ZHRDRGZGlPaWxPbkNxeGo5NmFiY0Q5SnVmeVJUYUFmSC9QbGJaaU5mOTVORWtxaXZmNC9CN2lqaEdIZlUwcXFNblJlYVdackFuK2M3RlByc2h6UWIrMC9OUSt1dWRMbGJ6czZHd1IyUG5sNmlNdjRsUFZ3d1UyTWprYzdMUzY5ZzZpSnBZZytsbDM1N0toSWhPUTRpMkNzVXhXWHlVZy9VZCtaQmhSTDFhWWxWQ1NaK1VRZlJodVNCREdkLzNrYjR0azNvUVMzaVgrS09EVFB5RGwxaTUweXhZdjJQZjRPdG1QR1RVYkdUeUJQd1RPUGN4TUR5dDBjWjAzdVdtY0MwblVrMllnWnlLeFFXTTBSaHJVK2pRUVZwL1BWY1NRdGYwREtTSDZzUEh2M1RlbmJMV25sdzNKaUlveEhmRjRJTGZhY255M0xPTGFxcXN1QU95REJmMnQxdlF2UTdkMmtZMGhwdUF4Y091c0I4dXpmdmQyTWtramZwVHFNWGN4TndNbUxWYXRobGVpQUUwRVBFaTFDanZuWVgyQUE5a2pwLy9oRlZaaklvK0ZYRzlQRE5ObVdDTVRPSXQwcmZoRzFxNTFDbG1sQW41Mm1vQ016d0lNOGlLVXk0MDNPdElBQkgrSzhTbHQ4aFpEaHRmRDBoR0xVOXg0TVBaUmhxMmdRd0tCL3Fpd3BnVnNWbDhrNmVxUnZpMjFjeWFJRklIRVQ3L0ljT25zWU1rYnh2azBQdGtIL0VUSHhsQjVqMUJ6TmJhQ0hJZ291bWJwVDdadlEyWjRESFhXNXJQdU55YVN6MEF2bTZ3dHljMDVxVTJVbXRWMXFOL1NOb203SkFkbms5ajg1TFUydUh1bVdqZHNLbVdFNXFLdWprcW16N3pEOE8yVGhuVzc3SWZQRngveG8yWXlaZUU2OXRFVlJCZ0dHUFA3R1NyaTVMaUl2TzZwTkZMak96QnRJTERnMUxFQUo1VUlqNjd4U0VzUjRIRW9CVEM1NHZKSUFoLzMxak0vUE96VFdkZ01YcXhlSlhGenE3ZHRqTWcrLzNSY0hIVm9LRGd3NjlrMmNnYXpwRzIwdVZuMTZkUUJYN0Jpdk12TVc3OStUb0xPcm0ydXhrN0VtUkNTVFUzMlFBbVZ6Z09mZHRKUDF2TWk0SU93aDVSYW51YWh2ekhIWDNHR3pRTkx4a1RiUXFFUjZmaHV0cEVVcEFOWmVMbnA2UzVaMkIwZGtVZ1BSeGc2NUpXY05OS1BSc0NrWkkxK1NTU1haeVMrOWFrUzhtd3c0NXRzdHRaTlZSR003RVh4YjUrU0FkMkwybFpLbnlNRll1M2lSZWcrSnZtMUIxVFZRL3lKejY0MFlLUzNMYitIQ3hoSmhTTGlhYk5Kb1Y3V1VuZTBGekt6bXVDdGRtR3BIWkM5cXh1SGdDcHRuVTJVb2oxNWF6Y2dBWHAyZjF6OHVUVCs2dXpxTm5lOVdzUHlwdE9NdlFhNDJzdmtZNlU5TkcvSi9VUTdRRmUrL2VUNnp6ZGQ2a2lSNzA1ZXBVeTA5MkpIekQrOWJDMmxtYWk4RGo0U3o0MVhUWmEvTHYzdXQwOTk1L3ptb3Z1R2M2VTdXMmFTSG9LcUs4dXRKeEliVmtKb3hYbFFuckVIMlM1YXc3WG1lREtwbzVwYXlLTU56eXhJeGxoTmxsRUVqbVdnODFBajRLbmFsOUR3N09sVVhrWE02eWFqWjNqaUN2Uk9RQVRVbGVid2gzWFppTnJtc1J4b3lWMVg5OWEzaWN1THlpRUJBVkRYQ0kwcTJqYWdCc1h4L3Nrci9oa2lhYkZqSCs3MUVvVWVjRm11RmhvcGxMak1td0tsSWFwTk02NENUaGRpdGdoUklUTFVDb2ZGYUQxOHd6bldlaEZrSFlVUW1JWXRFdzJYb005V2FMN1Fod0ZoeHVhL3FScFRLUTMxOXBNWk1qN2UrMlhaYk93Y0VLYnE2MFhRSllHaDNGMTUxSENYVW9lRHY1Nm42Yk52ckU2YVdkdkFEa3F1RWw3RTNYSmdueTlXOVJCSnMvMEo4QkxjNnlPOWk3V3ZIVVFwN3JRZkhGWklMbEIwSHgrcXhVWG1LWG5KZTczcGhSY0tTNjVIb3d4WjYreHppclFHTXhtci85R05VUEd5TVROR1ArRGlKbW1La3FMT09jd0NFSFNuZnl1NmExN24rM1l3U1g5NitNbkVmdTIwckhPb1pqVVZuT1ptRkNFRFFYZ0s3NWR5b3BzclZlM0pLZHBmYVFOVDllT0dvNk1qZDNiS3UrTVZDR3g4TitrNWE3ajJ3cGxtTVhuTVB6Q1JmcEkzd1pQeTRQc1VhVTEyS2xvaUZFT3poZTJGMk9EYUwxU3lGY2RueWhyRTgvcnZSc3pVV2R3UjZEai9LaVh0OC9vSm5aY2R5bDdjKzZSNi9HenRlSHZqY0oya1BnUk9nTURpUDFlTmVkNzJ0UmtzQjllNXZsczNZRnNrNWlBa1hiNEw5MjRibGx6c1VXQXBCeFRkMXFLS1hYZzhQVFhqanNCYTQ5dnNBNUVvaFZmbUUwMnR2NHNjQkZMaU5VcXdZY0dxZ25KbEFQQ3FaOGdnT1E3VnhWZFZuMVlRZkJPMm1vVGdaU0wrQ0NUeE9qRXlwSkR2RnM5Z01JZ0l3V0hBSFhNOGdQanZucXBsN1d3bXh0a2l3aUdEU1V6bjFscHViTEV2czdxYWxNZ2hieXVuVzRlcU5WdlBmUmlNMlllSGhheUF6dFF5MXFXaTJ6cGg1VWExWjFKQVREaW9FanBFMHNTN0FlZEdPNmovVzFmZDV6R1pibHlIN1M5VDY2ZEU0OHRjaFo0R0YxVmsvZlFYSGRRT0lkb1pabUE1T05yS0FuQWVWOEh2QUFxa29Nd3J4ZGFXN1lYMWNHSitXTUdOR08xU0VLVWczMldmUG1pY1hLTG9JWDBEUW1OYmd3TjNFenVtMm1uNjNNUTN2amVqckVaRnpsUmhKbStDYVMwTGJxVDVIRkRWWVBOb2xOZjl3aEFxMzAvSFZaaWt6UHhWM3lJQmlrdmE1QTFuSmZWcFhZRXFNa2IvaDR0V05iNXN3U2pucUVEOHViOU5Pc3RIcmVZaE9RYnBLelVHU2RjTkF1K1IrVDJzUlJBQzQ2TlZadmZOcVZ5UU90YVh0czhRdzRXdEJCaXpKS0xtLzBYd1oxMXdNZmtqci9nckg0cmllRGEwZTd1S2tQV3pZOXJqc2lpQ2MwL1ozUjBIM1hLNTVTOTB1bjVIVitRbkt1eHlyYk03UGhFUXVnRVE2TlQ3cVhvV1U1b3BSL1p2NmloSEhYMzB1ZStFYW53SmdlUUN4WGpsTlZEUGY2WEpFNXQ4eWxLMFd5andOQWZpdzdiZ2F3MVk5YVU1MU5ZZUc3QXo5RldXSWhHVGpmSXZMMThVY0pQWndib1hqNHZHMkdyc01YRDNMUjFKV2xJZGIwL29uTndvOENZYncrOFlhc3RtOUw0dzlDQUQzTHZRVzBBQ3dkS3hMQ1NycW5UWmw0YStTbHUvNXFTN2RIdzlBSGpMVnFFRGtLWU9qNnFEVHdpTWFzNkR4bEt0RmxXa2wyaWZFaFl0UGVERmg5d2ZOdHFhUjdBRFZtcWQyL3p0aFpzcmlaTVpvdmluMlJSWGptSTZsRGRkMFhScFhNV2hWQkUrN1JKZ0VRZTNzV28yS3d2TUtZMS9PcWVXYkxndUZZSVZTZ0w4NkFTU3g8L3hlbmM6Q2lwaGVyVmFsdWU%2BPC94ZW5jOkNpcGhlckRhdGE%2BPC94ZW5jOkVuY3J5cHRlZERhdGE%2BPC9zYW1sMjpFbmNyeXB0ZWRBc3NlcnRpb24%2BPC9zYW1sMnA6UmVzcG9uc2U%2B \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index 0db38295e1..4b431069fd 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -32,15 +32,8 @@ from third_party_auth.tests import testutil class IntegrationTest(testutil.TestCase, test.TestCase): """Abstract base class for provider integration tests.""" - # Configuration. You will need to override these values in your test cases. - - # Class. The third_party_auth.provider.BaseProvider child we are testing. - PROVIDER_CLASS = None - - # Dict of string -> object. Settings that will be merged onto Django's - # settings object before test execution. In most cases, this is - # PROVIDER_CLASS.SETTINGS with test values. - PROVIDER_SETTINGS = {} + # Override setUp and set this: + provider = None # Methods you must override in your children. @@ -94,10 +87,10 @@ class IntegrationTest(testutil.TestCase, test.TestCase): """ self.assertEqual(200, response.status_code) # Check that the correct provider was selected. - self.assertIn('successfully signed in with %s' % self.PROVIDER_CLASS.NAME, response.content) + self.assertIn('successfully signed in with %s' % self.provider.name, response.content) # Expect that each truthy value we've prepopulated the register form # with is actually present. - for prepopulated_form_value in self.PROVIDER_CLASS.get_register_form_data(pipeline_kwargs).values(): + for prepopulated_form_value in self.provider.get_register_form_data(pipeline_kwargs).values(): if prepopulated_form_value: self.assertIn(prepopulated_form_value, response.content) @@ -106,27 +99,30 @@ class IntegrationTest(testutil.TestCase, test.TestCase): def setUp(self): super(IntegrationTest, self).setUp() - self.configure_runtime() - self.backend_name = self.PROVIDER_CLASS.BACKEND_CLASS.name - self.client = test.Client() self.request_factory = test.RequestFactory() - def assert_account_settings_context_looks_correct(self, context, user, duplicate=False, linked=None): + @property + def backend_name(self): + """ Shortcut for the backend name """ + return self.provider.backend_name + + # pylint: disable=invalid-name + def assert_account_settings_context_looks_correct(self, context, _user, duplicate=False, linked=None): """Asserts the user's account settings page context is in the expected state. If duplicate is True, we expect context['duplicate_provider'] to contain - the duplicate provider object. If linked is passed, we conditionally + the duplicate provider backend name. If linked is passed, we conditionally check that the provider is included in context['auth']['providers'] and its connected state is correct. """ if duplicate: - self.assertEqual(context['duplicate_provider'].NAME, self.PROVIDER_CLASS.NAME) + self.assertEqual(context['duplicate_provider'], self.provider.backend_name) else: self.assertIsNone(context['duplicate_provider']) if linked is not None: expected_provider = [ - provider for provider in context['auth']['providers'] if provider['name'] == self.PROVIDER_CLASS.NAME + provider for provider in context['auth']['providers'] if provider['name'] == self.provider.name ][0] self.assertIsNotNone(expected_provider) self.assertEqual(expected_provider['connected'], linked) @@ -197,7 +193,10 @@ class IntegrationTest(testutil.TestCase, test.TestCase): def assert_json_failure_response_is_missing_social_auth(self, response): """Asserts failure on /login for missing social auth looks right.""" self.assertEqual(403, response.status_code) - self.assertIn("successfully logged into your %s account, but this account isn't linked" % self.PROVIDER_CLASS.NAME, response.content) + self.assertIn( + "successfully logged into your %s account, but this account isn't linked" % self.provider.name, + response.content + ) def assert_json_failure_response_is_username_collision(self, response): """Asserts the json response indicates a username collision.""" @@ -211,7 +210,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): self.assertEqual(200, response.status_code) payload = json.loads(response.content) self.assertTrue(payload.get('success')) - self.assertEqual(pipeline.get_complete_url(self.PROVIDER_CLASS.BACKEND_CLASS.name), payload.get('redirect_url')) + self.assertEqual(pipeline.get_complete_url(self.provider.backend_name), payload.get('redirect_url')) def assert_login_response_before_pipeline_looks_correct(self, response): """Asserts a GET of /login not in the pipeline looks correct.""" @@ -219,7 +218,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): # The combined login/registration page dynamically generates the login button, # but we can still check that the provider name is passed in the data attribute # for the container element. - self.assertIn(self.PROVIDER_CLASS.NAME, response.content) + self.assertIn(self.provider.name, response.content) def assert_login_response_in_pipeline_looks_correct(self, response): """Asserts a GET of /login in the pipeline looks correct.""" @@ -258,28 +257,21 @@ class IntegrationTest(testutil.TestCase, test.TestCase): # The combined login/registration page dynamically generates the register button, # but we can still check that the provider name is passed in the data attribute # for the container element. - self.assertIn(self.PROVIDER_CLASS.NAME, response.content) + self.assertIn(self.provider.name, response.content) def assert_social_auth_does_not_exist_for_user(self, user, strategy): """Asserts a user does not have an auth with the expected provider.""" social_auths = strategy.storage.user.get_social_auth_for_user( - user, provider=self.PROVIDER_CLASS.BACKEND_CLASS.name) + user, provider=self.provider.backend_name) self.assertEqual(0, len(social_auths)) def assert_social_auth_exists_for_user(self, user, strategy): """Asserts a user has a social auth with the expected provider.""" social_auths = strategy.storage.user.get_social_auth_for_user( - user, provider=self.PROVIDER_CLASS.BACKEND_CLASS.name) + user, provider=self.provider.backend_name) self.assertEqual(1, len(social_auths)) self.assertEqual(self.backend_name, social_auths[0].provider) - def configure_runtime(self): - """Configures settings details.""" - auth_settings.apply_settings({self.PROVIDER_CLASS.NAME: self.PROVIDER_SETTINGS}, django_settings) - # Force settings to propagate into cached members on - # social.apps.django_app.utils. - reload(social_utils) - def create_user_models_for_existing_account(self, strategy, email, password, username, skip_social_auth=False): """Creates user, profile, registration, and (usually) social auth. @@ -296,7 +288,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): registration.save() if not skip_social_auth: - social_utils.Storage.user.create_social_auth(user, uid, self.PROVIDER_CLASS.BACKEND_CLASS.name) + social_utils.Storage.user.create_social_auth(user, uid, self.provider.backend_name) return user @@ -370,13 +362,17 @@ class IntegrationTest(testutil.TestCase, test.TestCase): self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], - pipeline.get_complete_url(self.PROVIDER_CLASS.BACKEND_CLASS.name) + pipeline.get_complete_url(self.provider.backend_name) ) - self.assertEqual(response.cookies[django_settings.EDXMKTG_COOKIE_NAME].value, 'true') + self.assertEqual(response.cookies[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME].value, 'true') + self.assertIn(django_settings.EDXMKTG_USER_INFO_COOKIE_NAME, response.cookies) - def set_logged_in_cookie(self, request): + def set_logged_in_cookies(self, request): """Simulate setting the marketing site cookie on the request. """ - request.COOKIES[django_settings.EDXMKTG_COOKIE_NAME] = 'true' + request.COOKIES[django_settings.EDXMKTG_LOGGED_IN_COOKIE_NAME] = 'true' + request.COOKIES[django_settings.EDXMKTG_USER_INFO_COOKIE_NAME] = json.dumps({ + 'version': django_settings.EDXMKTG_USER_INFO_COOKIE_VERSION, + }) # Actual tests, executed once per child. @@ -413,7 +409,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): # Instrument the pipeline to get to the dashboard with the full # expected state. self.client.get( - pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN)) + pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) actions.do_complete(request.backend, social_views._do_login) # pylint: disable=protected-access mako_middleware_process_request(strategy.request) @@ -434,7 +430,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): )) # Set the cookie and try again - self.set_logged_in_cookie(request) + self.set_logged_in_cookies(request) # Fire off the auth pipeline to link. self.assert_redirect_to_dashboard_looks_correct(actions.do_complete( @@ -456,12 +452,12 @@ class IntegrationTest(testutil.TestCase, test.TestCase): self.assert_social_auth_exists_for_user(user, strategy) # We're already logged in, so simulate that the cookie is set correctly - self.set_logged_in_cookie(request) + self.set_logged_in_cookies(request) # Instrument the pipeline to get to the dashboard with the full # expected state. self.client.get( - pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN)) + pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) actions.do_complete(request.backend, social_views._do_login) # pylint: disable=protected-access mako_middleware_process_request(strategy.request) @@ -520,7 +516,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): self.assert_social_auth_exists_for_user(user, strategy) self.client.get('/login') - self.client.get(pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN)) + self.client.get(pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN)) actions.do_complete(request.backend, social_views._do_login) # pylint: disable=protected-access mako_middleware_process_request(strategy.request) @@ -532,7 +528,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): request._messages = fallback.FallbackStorage(request) middleware.ExceptionMiddleware().process_exception( request, - exceptions.AuthAlreadyAssociated(self.PROVIDER_CLASS.BACKEND_CLASS.name, 'account is already in use.')) + exceptions.AuthAlreadyAssociated(self.provider.backend_name, 'account is already in use.')) self.assert_account_settings_context_looks_correct( account_settings_context(request), user, duplicate=True, linked=True) @@ -557,7 +553,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( - pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN))) + pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/ # to resume the pipeline. @@ -582,7 +578,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): )) # Set the cookie and try again - self.set_logged_in_cookie(request) + self.set_logged_in_cookies(request) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(request.backend, social_views._do_login, user=user)) @@ -637,7 +633,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): # Synthesize that request and check that it redirects to the correct # provider page. self.assert_redirect_to_provider_looks_correct(self.client.get( - pipeline.get_login_url(self.PROVIDER_CLASS.NAME, pipeline.AUTH_ENTRY_LOGIN))) + pipeline.get_login_url(self.provider.provider_id, pipeline.AUTH_ENTRY_LOGIN))) # Next, the provider makes a request against /auth/complete/. # pylint: disable=protected-access @@ -683,7 +679,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase): )) # Set the cookie and try again - self.set_logged_in_cookie(request) + self.set_logged_in_cookies(request) self.assert_redirect_to_dashboard_looks_correct( actions.do_complete(strategy.request.backend, social_views._do_login, user=created_user)) # Now the user has been redirected to the dashboard. Their third party account should now be linked. diff --git a/common/djangoapps/third_party_auth/tests/specs/test_google.py b/common/djangoapps/third_party_auth/tests/specs/test_google.py index 320739b81e..d591c1e594 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_google.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_google.py @@ -7,11 +7,14 @@ from third_party_auth.tests.specs import base class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest): """Integration tests for provider.GoogleOauth2.""" - PROVIDER_CLASS = provider.GoogleOauth2 - PROVIDER_SETTINGS = { - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_oauth2_key', - 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': 'google_oauth2_secret', - } + def setUp(self): + super(GoogleOauth2IntegrationTest, self).setUp() + self.provider = self.configure_google_provider( + enabled=True, + key='google_oauth2_key', + secret='google_oauth2_secret', + ) + TOKEN_RESPONSE_DATA = { 'access_token': 'access_token_value', 'expires_in': 'expires_in_value', diff --git a/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py b/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py index e51cc2ecc7..c149065115 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_linkedin.py @@ -7,11 +7,14 @@ from third_party_auth.tests.specs import base class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest): """Integration tests for provider.LinkedInOauth2.""" - PROVIDER_CLASS = provider.LinkedInOauth2 - PROVIDER_SETTINGS = { - 'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': 'linkedin_oauth2_key', - 'SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET': 'linkedin_oauth2_secret', - } + def setUp(self): + super(LinkedInOauth2IntegrationTest, self).setUp() + self.provider = self.configure_linkedin_provider( + enabled=True, + key='linkedin_oauth2_key', + secret='linkedin_oauth2_secret', + ) + TOKEN_RESPONSE_DATA = { 'access_token': 'access_token_value', 'expires_in': 'expires_in_value', diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py new file mode 100644 index 0000000000..aacb945aa6 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -0,0 +1,230 @@ +""" +Third_party_auth integration tests using a mock version of the TestShib provider +""" +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +import httpretty +from mock import patch +from student.tests.factories import UserFactory +from third_party_auth.tasks import fetch_saml_metadata +from third_party_auth.tests import testutil +import unittest + +TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth' +TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml' +TESTSHIB_SSO_URL = 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO' + +TPA_TESTSHIB_LOGIN_URL = '/auth/login/tpa-saml/?auth_entry=login&next=%2Fdashboard&idp=testshib' +TPA_TESTSHIB_REGISTER_URL = '/auth/login/tpa-saml/?auth_entry=register&next=%2Fdashboard&idp=testshib' +TPA_TESTSHIB_COMPLETE_URL = '/auth/complete/tpa-saml/' + + +@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') +class TestShibIntegrationTest(testutil.SAMLTestCase): + """ + TestShib provider Integration Test, to test SAML functionality + """ + def setUp(self): + super(TestShibIntegrationTest, self).setUp() + self.login_page_url = reverse('signin_user') + self.register_page_url = reverse('register_user') + self.enable_saml( + private_key=self._get_private_key(), + public_key=self._get_public_key(), + entity_id="https://saml.example.none", + ) + # Mock out HTTP requests that may be made to TestShib: + httpretty.enable() + + def metadata_callback(_request, _uri, headers): + """ Return a cached copy of TestShib's metadata by reading it from disk """ + return (200, headers, self._read_data_file('testshib_metadata.xml')) + httpretty.register_uri(httpretty.GET, TESTSHIB_METADATA_URL, content_type='text/xml', body=metadata_callback) + self.addCleanup(httpretty.disable) + self.addCleanup(httpretty.reset) + + # Configure the SAML library to use the same request ID for every request. + # Doing this and freezing the time allows us to play back recorded request/response pairs + uid_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id', return_value='TESTID') + uid_patch.start() + self.addCleanup(uid_patch.stop) + + def test_login_before_metadata_fetched(self): + self._configure_testshib_provider(fetch_metadata=False) + # The user goes to the login page, and sees a button to login with TestShib: + self._check_login_page() + # The user clicks on the TestShib button: + try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) + # The user should be redirected to back to the login page: + self.assertEqual(try_login_response.status_code, 302) + self.assertEqual(try_login_response['Location'], self.url_prefix + self.login_page_url) + # When loading the login page, the user will see an error message: + response = self.client.get(self.login_page_url) + self.assertEqual(response.status_code, 200) + self.assertIn('Authentication with TestShib is currently unavailable.', response.content) + + def test_register(self): + self._configure_testshib_provider() + self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. + # The user goes to the register page, and sees a button to register with TestShib: + self._check_register_page() + # The user clicks on the TestShib button: + try_login_response = self.client.get(TPA_TESTSHIB_REGISTER_URL) + # The user should be redirected to TestShib: + self.assertEqual(try_login_response.status_code, 302) + self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) + # Now the user will authenticate with the SAML provider + testshib_response = self._fake_testshib_login_and_return() + # We should be redirected to the register screen since this account is not linked to an edX account: + self.assertEqual(testshib_response.status_code, 302) + self.assertEqual(testshib_response['Location'], self.url_prefix + self.register_page_url) + register_response = self.client.get(self.register_page_url) + # We'd now like to see if the "You've successfully signed into TestShib" message is + # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this + # type of test, so we just check for the variable that triggers that message. + self.assertIn('"currentProvider": "TestShib"', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) + # Now do a crude check that the data (e.g. email) from the provider is displayed in the form: + self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) + self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) + # Now complete the form: + ajax_register_response = self.client.post( + reverse('user_api_registration'), + { + 'email': 'myself@testshib.org', + 'name': 'Myself', + 'username': 'myself', + 'honor_code': True, + } + ) + self.assertEqual(ajax_register_response.status_code, 200) + # Then the AJAX will finish the third party auth: + continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) + # And we should be redirected to the dashboard: + self.assertEqual(continue_response.status_code, 302) + self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + + # Now check that we can login again: + self.client.logout() + self._verify_user_email('myself@testshib.org') + self._test_return_login() + + def test_login(self): + self._configure_testshib_provider() + self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. + user = UserFactory.create() + # The user goes to the login page, and sees a button to login with TestShib: + self._check_login_page() + # The user clicks on the TestShib button: + try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) + # The user should be redirected to TestShib: + self.assertEqual(try_login_response.status_code, 302) + self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) + # Now the user will authenticate with the SAML provider + testshib_response = self._fake_testshib_login_and_return() + # We should be redirected to the login screen since this account is not linked to an edX account: + self.assertEqual(testshib_response.status_code, 302) + self.assertEqual(testshib_response['Location'], self.url_prefix + self.login_page_url) + login_response = self.client.get(self.login_page_url) + # We'd now like to see if the "You've successfully signed into TestShib" message is + # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this + # type of test, so we just check for the variable that triggers that message. + self.assertIn('"currentProvider": "TestShib"', login_response.content) + self.assertIn('"errorMessage": null', login_response.content) + # Now the user enters their username and password. + # The AJAX on the page will log them in: + ajax_login_response = self.client.post( + reverse('user_api_login_session'), + {'email': user.email, 'password': 'test'} + ) + self.assertEqual(ajax_login_response.status_code, 200) + # Then the AJAX will finish the third party auth: + continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) + # And we should be redirected to the dashboard: + self.assertEqual(continue_response.status_code, 302) + self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + + # Now check that we can login again: + self.client.logout() + self._test_return_login() + + def _test_return_login(self): + """ Test logging in to an account that is already linked. """ + # Make sure we're not logged in: + dashboard_response = self.client.get(reverse('dashboard')) + self.assertEqual(dashboard_response.status_code, 302) + # The user goes to the login page, and sees a button to login with TestShib: + self._check_login_page() + # The user clicks on the TestShib button: + try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) + # The user should be redirected to TestShib: + self.assertEqual(try_login_response.status_code, 302) + self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) + # Now the user will authenticate with the SAML provider + login_response = self._fake_testshib_login_and_return() + # There will be one weird redirect required to set the login cookie: + self.assertEqual(login_response.status_code, 302) + self.assertEqual(login_response['Location'], self.url_prefix + TPA_TESTSHIB_COMPLETE_URL) + # And then we should be redirected to the dashboard: + login_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) + self.assertEqual(login_response.status_code, 302) + self.assertEqual(login_response['Location'], self.url_prefix + reverse('dashboard')) + # Now we are logged in: + dashboard_response = self.client.get(reverse('dashboard')) + self.assertEqual(dashboard_response.status_code, 200) + + def _freeze_time(self, timestamp): + """ Mock the current time for SAML, so we can replay canned requests/responses """ + now_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.now', return_value=timestamp) + now_patch.start() + self.addCleanup(now_patch.stop) + + def _check_login_page(self): + """ Load the login form and check that it contains a TestShib button """ + response = self.client.get(self.login_page_url) + self.assertEqual(response.status_code, 200) + self.assertIn("TestShib", response.content) + self.assertIn(TPA_TESTSHIB_LOGIN_URL.replace('&', '&'), response.content) + return response + + def _check_register_page(self): + """ Load the login form and check that it contains a TestShib button """ + response = self.client.get(self.register_page_url) + self.assertEqual(response.status_code, 200) + self.assertIn("TestShib", response.content) + self.assertIn(TPA_TESTSHIB_REGISTER_URL.replace('&', '&'), response.content) + return response + + def _configure_testshib_provider(self, **kwargs): + """ Enable and configure the TestShib SAML IdP as a third_party_auth provider """ + fetch_metadata = kwargs.pop('fetch_metadata', True) + kwargs.setdefault('name', 'TestShib') + kwargs.setdefault('enabled', True) + kwargs.setdefault('idp_slug', 'testshib') + kwargs.setdefault('entity_id', TESTSHIB_ENTITY_ID) + kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL) + kwargs.setdefault('icon_class', 'fa-university') + kwargs.setdefault('attr_email', 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6') # eduPersonPrincipalName + self.configure_saml_provider(**kwargs) + + if fetch_metadata: + self.assertTrue(httpretty.is_enabled()) + num_changed, num_failed, num_total = fetch_saml_metadata() + self.assertEqual(num_failed, 0) + self.assertEqual(num_changed, 1) + self.assertEqual(num_total, 1) + + def _fake_testshib_login_and_return(self): + """ Mocked: the user logs in to TestShib and then gets redirected back """ + # The SAML provider (TestShib) will authenticate the user, then get the browser to POST a response: + return self.client.post( + TPA_TESTSHIB_COMPLETE_URL, + content_type='application/x-www-form-urlencoded', + data=self._read_data_file('testshib_response.txt'), + ) + + def _verify_user_email(self, email): + """ Mark the user with the given email as verified """ + user = User.objects.get(email=email) + user.is_active = True + user.save() diff --git a/common/djangoapps/third_party_auth/tests/test_pipeline.py b/common/djangoapps/third_party_auth/tests/test_pipeline.py index 66c11d9043..c4387626ea 100644 --- a/common/djangoapps/third_party_auth/tests/test_pipeline.py +++ b/common/djangoapps/third_party_auth/tests/test_pipeline.py @@ -4,6 +4,7 @@ import random from third_party_auth import pipeline, provider from third_party_auth.tests import testutil +import unittest # Allow tests access to protected methods (or module-protected methods) under @@ -34,9 +35,11 @@ class MakeRandomPasswordTest(testutil.TestCase): self.assertEqual(expected, pipeline.make_random_password(choice_fn=random_instance.choice)) +@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') class ProviderUserStateTestCase(testutil.TestCase): """Tests ProviderUserState behavior.""" def test_get_unlink_form_name(self): - state = pipeline.ProviderUserState(provider.GoogleOauth2, object(), False) - self.assertEqual(provider.GoogleOauth2.NAME + '_unlink_form', state.get_unlink_form_name()) + google_provider = self.configure_google_provider(enabled=True) + state = pipeline.ProviderUserState(google_provider, object(), 1000) + self.assertEqual(google_provider.provider_id + '_unlink_form', state.get_unlink_form_name()) diff --git a/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py b/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py index 8d1f3b7019..d21d834c93 100644 --- a/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py +++ b/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py @@ -21,9 +21,7 @@ class TestCase(testutil.TestCase, test.TestCase): def setUp(self): super(TestCase, self).setUp() - self.enabled_provider_name = provider.GoogleOauth2.NAME - provider.Registry.configure_once([self.enabled_provider_name]) - self.enabled_provider = provider.Registry.get(self.enabled_provider_name) + self.enabled_provider = self.configure_google_provider(enabled=True) @unittest.skipUnless( @@ -41,28 +39,28 @@ class GetAuthenticatedUserTestCase(TestCase): def test_raises_does_not_exist_if_user_missing(self): with self.assertRaises(models.User.DoesNotExist): - pipeline.get_authenticated_user('new_' + self.user.username, 'backend') + pipeline.get_authenticated_user(self.enabled_provider, 'new_' + self.user.username, 'user@example.com') def test_raises_does_not_exist_if_user_found_but_no_association(self): backend_name = 'backend' self.assertIsNotNone(self.get_by_username(self.user.username)) - self.assertIsNone(provider.Registry.get_by_backend_name(backend_name)) + self.assertFalse(any(provider.Registry.get_enabled_by_backend_name(backend_name))) with self.assertRaises(models.User.DoesNotExist): - pipeline.get_authenticated_user(self.user.username, 'backend') + pipeline.get_authenticated_user(self.enabled_provider, self.user.username, 'user@example.com') def test_raises_does_not_exist_if_user_and_association_found_but_no_match(self): self.assertIsNotNone(self.get_by_username(self.user.username)) social_models.DjangoStorage.user.create_social_auth( - self.user, 'uid', 'other_' + self.enabled_provider.BACKEND_CLASS.name) + self.user, 'uid', 'other_' + self.enabled_provider.backend_name) with self.assertRaises(models.User.DoesNotExist): - pipeline.get_authenticated_user(self.user.username, self.enabled_provider.BACKEND_CLASS.name) + pipeline.get_authenticated_user(self.enabled_provider, self.user.username, 'uid') def test_returns_user_with_is_authenticated_and_backend_set_if_match(self): - social_models.DjangoStorage.user.create_social_auth(self.user, 'uid', self.enabled_provider.BACKEND_CLASS.name) - user = pipeline.get_authenticated_user(self.user.username, self.enabled_provider.BACKEND_CLASS.name) + social_models.DjangoStorage.user.create_social_auth(self.user, 'uid', self.enabled_provider.backend_name) + user = pipeline.get_authenticated_user(self.enabled_provider, self.user.username, 'uid') self.assertEqual(self.user, user) self.assertEqual(self.enabled_provider.get_authentication_backend(), user.backend) @@ -78,55 +76,70 @@ class GetProviderUserStatesTestCase(testutil.TestCase, test.TestCase): self.user = social_models.DjangoStorage.user.create_user(username='username', password='password') def test_returns_empty_list_if_no_enabled_providers(self): - provider.Registry.configure_once([]) + self.assertFalse(provider.Registry.enabled()) self.assertEquals([], pipeline.get_provider_user_states(self.user)) def test_state_not_returned_for_disabled_provider(self): - disabled_provider = provider.GoogleOauth2 - enabled_provider = provider.LinkedInOauth2 - provider.Registry.configure_once([enabled_provider.NAME]) - social_models.DjangoStorage.user.create_social_auth(self.user, 'uid', disabled_provider.BACKEND_CLASS.name) + disabled_provider = self.configure_google_provider(enabled=False) + enabled_provider = self.configure_facebook_provider(enabled=True) + social_models.DjangoStorage.user.create_social_auth(self.user, 'uid', disabled_provider.backend_name) states = pipeline.get_provider_user_states(self.user) self.assertEqual(1, len(states)) - self.assertNotIn(disabled_provider, (state.provider for state in states)) + self.assertNotIn(disabled_provider.provider_id, (state.provider.provider_id for state in states)) + self.assertIn(enabled_provider.provider_id, (state.provider.provider_id for state in states)) def test_states_for_enabled_providers_user_has_accounts_associated_with(self): - provider.Registry.configure_once([provider.GoogleOauth2.NAME, provider.LinkedInOauth2.NAME]) - social_models.DjangoStorage.user.create_social_auth(self.user, 'uid', provider.GoogleOauth2.BACKEND_CLASS.name) - social_models.DjangoStorage.user.create_social_auth( - self.user, 'uid', provider.LinkedInOauth2.BACKEND_CLASS.name) + # Enable two providers - Google and LinkedIn: + google_provider = self.configure_google_provider(enabled=True) + linkedin_provider = self.configure_linkedin_provider(enabled=True) + user_social_auth_google = social_models.DjangoStorage.user.create_social_auth( + self.user, 'uid', google_provider.backend_name) + user_social_auth_linkedin = social_models.DjangoStorage.user.create_social_auth( + self.user, 'uid', linkedin_provider.backend_name) states = pipeline.get_provider_user_states(self.user) self.assertEqual(2, len(states)) - google_state = [state for state in states if state.provider == provider.GoogleOauth2][0] - linkedin_state = [state for state in states if state.provider == provider.LinkedInOauth2][0] + google_state = [state for state in states if state.provider.provider_id == google_provider.provider_id][0] + linkedin_state = [state for state in states if state.provider.provider_id == linkedin_provider.provider_id][0] self.assertTrue(google_state.has_account) - self.assertEqual(provider.GoogleOauth2, google_state.provider) + self.assertEqual(google_provider.provider_id, google_state.provider.provider_id) + # Also check the row ID. Note this 'id' changes whenever the configuration does: + self.assertEqual(google_provider.id, google_state.provider.id) # pylint: disable=no-member self.assertEqual(self.user, google_state.user) + self.assertEqual(user_social_auth_google.id, google_state.association_id) self.assertTrue(linkedin_state.has_account) - self.assertEqual(provider.LinkedInOauth2, linkedin_state.provider) + self.assertEqual(linkedin_provider.provider_id, linkedin_state.provider.provider_id) + self.assertEqual(linkedin_provider.id, linkedin_state.provider.id) # pylint: disable=no-member self.assertEqual(self.user, linkedin_state.user) + self.assertEqual(user_social_auth_linkedin.id, linkedin_state.association_id) def test_states_for_enabled_providers_user_has_no_account_associated_with(self): - provider.Registry.configure_once([provider.GoogleOauth2.NAME, provider.LinkedInOauth2.NAME]) + # Enable two providers - Google and LinkedIn: + google_provider = self.configure_google_provider(enabled=True) + linkedin_provider = self.configure_linkedin_provider(enabled=True) + self.assertEqual(len(provider.Registry.enabled()), 2) + states = pipeline.get_provider_user_states(self.user) self.assertEqual([], [x for x in social_models.DjangoStorage.user.objects.all()]) self.assertEqual(2, len(states)) - google_state = [state for state in states if state.provider == provider.GoogleOauth2][0] - linkedin_state = [state for state in states if state.provider == provider.LinkedInOauth2][0] + google_state = [state for state in states if state.provider.provider_id == google_provider.provider_id][0] + linkedin_state = [state for state in states if state.provider.provider_id == linkedin_provider.provider_id][0] self.assertFalse(google_state.has_account) - self.assertEqual(provider.GoogleOauth2, google_state.provider) + self.assertEqual(google_provider.provider_id, google_state.provider.provider_id) + # Also check the row ID. Note this 'id' changes whenever the configuration does: + self.assertEqual(google_provider.id, google_state.provider.id) # pylint: disable=no-member self.assertEqual(self.user, google_state.user) self.assertFalse(linkedin_state.has_account) - self.assertEqual(provider.LinkedInOauth2, linkedin_state.provider) + self.assertEqual(linkedin_provider.provider_id, linkedin_state.provider.provider_id) + self.assertEqual(linkedin_provider.id, linkedin_state.provider.id) # pylint: disable=no-member self.assertEqual(self.user, linkedin_state.user) @@ -136,7 +149,7 @@ class UrlFormationTestCase(TestCase): """Tests formation of URLs for pipeline hook points.""" def test_complete_url_raises_value_error_if_provider_not_enabled(self): - provider_name = 'not_enabled' + provider_name = 'oa2-not-enabled' self.assertIsNone(provider.Registry.get(provider_name)) @@ -144,36 +157,54 @@ class UrlFormationTestCase(TestCase): pipeline.get_complete_url(provider_name) def test_complete_url_returns_expected_format(self): - complete_url = pipeline.get_complete_url(self.enabled_provider.BACKEND_CLASS.name) + complete_url = pipeline.get_complete_url(self.enabled_provider.backend_name) self.assertTrue(complete_url.startswith('/auth/complete')) - self.assertIn(self.enabled_provider.BACKEND_CLASS.name, complete_url) + self.assertIn(self.enabled_provider.backend_name, complete_url) def test_disconnect_url_raises_value_error_if_provider_not_enabled(self): - provider_name = 'not_enabled' + provider_name = 'oa2-not-enabled' self.assertIsNone(provider.Registry.get(provider_name)) with self.assertRaises(ValueError): - pipeline.get_disconnect_url(provider_name) + pipeline.get_disconnect_url(provider_name, 1000) def test_disconnect_url_returns_expected_format(self): - disconnect_url = pipeline.get_disconnect_url(self.enabled_provider.NAME) - - self.assertTrue(disconnect_url.startswith('/auth/disconnect')) - self.assertIn(self.enabled_provider.BACKEND_CLASS.name, disconnect_url) + disconnect_url = pipeline.get_disconnect_url(self.enabled_provider.provider_id, 1000) + disconnect_url = disconnect_url.rstrip('?') + self.assertEqual( + disconnect_url, + '/auth/disconnect/{backend}/{association_id}/'.format( + backend=self.enabled_provider.backend_name, association_id=1000) + ) def test_login_url_raises_value_error_if_provider_not_enabled(self): - provider_name = 'not_enabled' + provider_id = 'oa2-not-enabled' - self.assertIsNone(provider.Registry.get(provider_name)) + self.assertIsNone(provider.Registry.get(provider_id)) with self.assertRaises(ValueError): - pipeline.get_login_url(provider_name, pipeline.AUTH_ENTRY_LOGIN) + pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN) def test_login_url_returns_expected_format(self): - login_url = pipeline.get_login_url(self.enabled_provider.NAME, pipeline.AUTH_ENTRY_LOGIN) + login_url = pipeline.get_login_url(self.enabled_provider.provider_id, pipeline.AUTH_ENTRY_LOGIN) self.assertTrue(login_url.startswith('/auth/login')) - self.assertIn(self.enabled_provider.BACKEND_CLASS.name, login_url) + self.assertIn(self.enabled_provider.backend_name, login_url) self.assertTrue(login_url.endswith(pipeline.AUTH_ENTRY_LOGIN)) + + def test_for_value_error_if_provider_id_invalid(self): + provider_id = 'invalid' # Format is normally "{prefix}-{identifier}" + + with self.assertRaises(ValueError): + provider.Registry.get(provider_id) + + with self.assertRaises(ValueError): + pipeline.get_login_url(provider_id, pipeline.AUTH_ENTRY_LOGIN) + + with self.assertRaises(ValueError): + pipeline.get_disconnect_url(provider_id, 1000) + + with self.assertRaises(ValueError): + pipeline.get_complete_url(provider_id) diff --git a/common/djangoapps/third_party_auth/tests/test_provider.py b/common/djangoapps/third_party_auth/tests/test_provider.py index 20120d7329..bc3f71660a 100644 --- a/common/djangoapps/third_party_auth/tests/test_provider.py +++ b/common/djangoapps/third_party_auth/tests/test_provider.py @@ -1,82 +1,84 @@ """Unit tests for provider.py.""" +from mock import Mock, patch from third_party_auth import provider from third_party_auth.tests import testutil +import unittest +@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') class RegistryTest(testutil.TestCase): """Tests registry discovery and operation.""" - # Allow access to protected methods (or module-protected methods) under - # test. pylint: disable-msg=protected-access - - def test_calling_configure_once_twice_raises_value_error(self): - provider.Registry.configure_once([provider.GoogleOauth2.NAME]) - - with self.assertRaisesRegexp(ValueError, '^.*already configured$'): - provider.Registry.configure_once([provider.GoogleOauth2.NAME]) - def test_configure_once_adds_gettable_providers(self): - provider.Registry.configure_once([provider.GoogleOauth2.NAME]) - self.assertIs(provider.GoogleOauth2, provider.Registry.get(provider.GoogleOauth2.NAME)) + facebook_provider = self.configure_facebook_provider(enabled=True) + # pylint: disable=no-member + self.assertEqual(facebook_provider.id, provider.Registry.get(facebook_provider.provider_id).id) - def test_configuring_provider_with_no_implementation_raises_value_error(self): - with self.assertRaisesRegexp(ValueError, '^.*no_implementation$'): - provider.Registry.configure_once(['no_implementation']) + def test_no_providers_by_default(self): + enabled_providers = provider.Registry.enabled() + self.assertEqual(len(enabled_providers), 0, "By default, no providers are enabled.") - def test_configuring_single_provider_twice_raises_value_error(self): - provider.Registry._enable(provider.GoogleOauth2) + def test_runtime_configuration(self): + self.configure_google_provider(enabled=True) + enabled_providers = provider.Registry.enabled() + self.assertEqual(len(enabled_providers), 1) + self.assertEqual(enabled_providers[0].name, "Google") + self.assertEqual(enabled_providers[0].secret, "opensesame") - with self.assertRaisesRegexp(ValueError, '^.*already enabled'): - provider.Registry.configure_once([provider.GoogleOauth2.NAME]) + self.configure_google_provider(enabled=False) + enabled_providers = provider.Registry.enabled() + self.assertEqual(len(enabled_providers), 0) - def test_custom_provider_can_be_enabled(self): - name = 'CustomProvider' + self.configure_google_provider(enabled=True, secret="alohomora") + enabled_providers = provider.Registry.enabled() + self.assertEqual(len(enabled_providers), 1) + self.assertEqual(enabled_providers[0].secret, "alohomora") - with self.assertRaisesRegexp(ValueError, '^No implementation.*$'): - provider.Registry.configure_once([name]) - - class CustomProvider(provider.BaseProvider): - """Custom class to ensure BaseProvider children outside provider can be enabled.""" - - NAME = name - - provider.Registry._reset() - provider.Registry.configure_once([CustomProvider.NAME]) - self.assertEqual([CustomProvider], provider.Registry.enabled()) - - def test_enabled_raises_runtime_error_if_not_configured(self): - with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'): - provider.Registry.enabled() + def test_cannot_load_arbitrary_backends(self): + """ Test that only backend_names listed in settings.AUTHENTICATION_BACKENDS can be used """ + self.configure_oauth_provider(enabled=True, name="Disallowed", backend_name="disallowed") + self.enable_saml() + self.configure_saml_provider(enabled=True, name="Disallowed", idp_slug="test", backend_name="disallowed") + self.assertEqual(len(provider.Registry.enabled()), 0) def test_enabled_returns_list_of_enabled_providers_sorted_by_name(self): - all_providers = provider.Registry._get_all() - provider.Registry.configure_once(all_providers.keys()) - self.assertEqual( - sorted(all_providers.values(), key=lambda provider: provider.NAME), provider.Registry.enabled()) + provider_names = ["Stack Overflow", "Google", "LinkedIn", "GitHub"] + backend_names = [] + for name in provider_names: + backend_name = name.lower().replace(' ', '') + backend_names.append(backend_name) + self.configure_oauth_provider(enabled=True, name=name, backend_name=backend_name) - def test_get_raises_runtime_error_if_not_configured(self): - with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'): - provider.Registry.get('anything') + with patch('third_party_auth.provider._PSA_OAUTH2_BACKENDS', backend_names): + self.assertEqual(sorted(provider_names), [prov.name for prov in provider.Registry.enabled()]) def test_get_returns_enabled_provider(self): - provider.Registry.configure_once([provider.GoogleOauth2.NAME]) - self.assertIs(provider.GoogleOauth2, provider.Registry.get(provider.GoogleOauth2.NAME)) + google_provider = self.configure_google_provider(enabled=True) + # pylint: disable=no-member + self.assertEqual(google_provider.id, provider.Registry.get(google_provider.provider_id).id) def test_get_returns_none_if_provider_not_enabled(self): - provider.Registry.configure_once([]) - self.assertIsNone(provider.Registry.get(provider.LinkedInOauth2.NAME)) + linkedin_provider_id = "oa2-linkedin-oauth2" + # At this point there should be no configuration entries at all so no providers should be enabled + self.assertEqual(provider.Registry.enabled(), []) + self.assertIsNone(provider.Registry.get(linkedin_provider_id)) + # Now explicitly disabled this provider: + self.configure_linkedin_provider(enabled=False) + self.assertIsNone(provider.Registry.get(linkedin_provider_id)) + self.configure_linkedin_provider(enabled=True) + self.assertEqual(provider.Registry.get(linkedin_provider_id).provider_id, linkedin_provider_id) - def test_get_by_backend_name_raises_runtime_error_if_not_configured(self): - with self.assertRaisesRegexp(RuntimeError, '^.*not configured$'): - provider.Registry.get_by_backend_name('') + def test_get_from_pipeline_returns_none_if_provider_not_enabled(self): + self.assertEqual(provider.Registry.enabled(), [], "By default, no providers are enabled.") + self.assertIsNone(provider.Registry.get_from_pipeline(Mock())) - def test_get_by_backend_name_returns_enabled_provider(self): - provider.Registry.configure_once([provider.GoogleOauth2.NAME]) - self.assertIs( - provider.GoogleOauth2, - provider.Registry.get_by_backend_name(provider.GoogleOauth2.BACKEND_CLASS.name)) + def test_get_enabled_by_backend_name_returns_enabled_provider(self): + google_provider = self.configure_google_provider(enabled=True) + found = list(provider.Registry.get_enabled_by_backend_name(google_provider.backend_name)) + self.assertEqual(found, [google_provider]) - def test_get_by_backend_name_returns_none_if_provider_not_enabled(self): - provider.Registry.configure_once([]) - self.assertIsNone(provider.Registry.get_by_backend_name(provider.GoogleOauth2.BACKEND_CLASS.name)) + def test_get_enabled_by_backend_name_returns_none_if_provider_not_enabled(self): + google_provider = self.configure_google_provider(enabled=False) + found = list(provider.Registry.get_enabled_by_backend_name(google_provider.backend_name)) + self.assertEqual(found, []) diff --git a/common/djangoapps/third_party_auth/tests/test_settings.py b/common/djangoapps/third_party_auth/tests/test_settings.py index 40babdbc1c..1c1229190e 100644 --- a/common/djangoapps/third_party_auth/tests/test_settings.py +++ b/common/djangoapps/third_party_auth/tests/test_settings.py @@ -2,6 +2,7 @@ from third_party_auth import provider, settings from third_party_auth.tests import testutil +import unittest _ORIGINAL_AUTHENTICATION_BACKENDS = ('first_authentication_backend',) @@ -30,56 +31,26 @@ class SettingsUnitTest(testutil.TestCase): self.settings = testutil.FakeDjangoSettings(_SETTINGS_MAP) def test_apply_settings_adds_exception_middleware(self): - settings.apply_settings({}, self.settings) + settings.apply_settings(self.settings) for middleware_name in settings._MIDDLEWARE_CLASSES: self.assertIn(middleware_name, self.settings.MIDDLEWARE_CLASSES) def test_apply_settings_adds_fields_stored_in_session(self): - settings.apply_settings({}, self.settings) + settings.apply_settings(self.settings) self.assertEqual(settings._FIELDS_STORED_IN_SESSION, self.settings.FIELDS_STORED_IN_SESSION) def test_apply_settings_adds_third_party_auth_to_installed_apps(self): - settings.apply_settings({}, self.settings) + settings.apply_settings(self.settings) self.assertIn('third_party_auth', self.settings.INSTALLED_APPS) - def test_apply_settings_enables_no_providers_and_completes_when_app_info_empty(self): - settings.apply_settings({}, self.settings) + @unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') + def test_apply_settings_enables_no_providers_by_default(self): + # Providers are only enabled via ConfigurationModels in the database + settings.apply_settings(self.settings) self.assertEqual([], provider.Registry.enabled()) - def test_apply_settings_initializes_stubs_and_merges_settings_from_auth_info(self): - for key in provider.GoogleOauth2.SETTINGS: - self.assertFalse(hasattr(self.settings, key)) - - auth_info = { - provider.GoogleOauth2.NAME: { - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_oauth2_key', - }, - } - settings.apply_settings(auth_info, self.settings) - self.assertEqual('google_oauth2_key', self.settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY) - self.assertIsNone(self.settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET) - - def test_apply_settings_prepends_auth_backends(self): - self.assertEqual(_ORIGINAL_AUTHENTICATION_BACKENDS, self.settings.AUTHENTICATION_BACKENDS) - settings.apply_settings({provider.GoogleOauth2.NAME: {}, provider.LinkedInOauth2.NAME: {}}, self.settings) - self.assertEqual(( - provider.GoogleOauth2.get_authentication_backend(), provider.LinkedInOauth2.get_authentication_backend()) + - _ORIGINAL_AUTHENTICATION_BACKENDS, - self.settings.AUTHENTICATION_BACKENDS) - - def test_apply_settings_raises_value_error_if_provider_contains_uninitialized_setting(self): - bad_setting_name = 'bad_setting' - self.assertNotIn('bad_setting_name', provider.GoogleOauth2.SETTINGS) - auth_info = { - provider.GoogleOauth2.NAME: { - bad_setting_name: None, - }, - } - with self.assertRaisesRegexp(ValueError, '^.*not initialized$'): - settings.apply_settings(auth_info, self.settings) - def test_apply_settings_turns_off_raising_social_exceptions(self): # Guard against submitting a conf change that's convenient in dev but # bad in prod. - settings.apply_settings({}, self.settings) + settings.apply_settings(self.settings) self.assertFalse(self.settings.SOCIAL_AUTH_RAISE_EXCEPTIONS) diff --git a/common/djangoapps/third_party_auth/tests/test_settings_integration.py b/common/djangoapps/third_party_auth/tests/test_settings_integration.py deleted file mode 100644 index 8992f9fb79..0000000000 --- a/common/djangoapps/third_party_auth/tests/test_settings_integration.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Integration tests for settings.py.""" - -from django.conf import settings - -from third_party_auth import provider -from third_party_auth import settings as auth_settings -from third_party_auth.tests import testutil - - -class SettingsIntegrationTest(testutil.TestCase): - """Integration tests of auth settings pipeline. - - Note that ENABLE_THIRD_PARTY_AUTH is True in lms/envs/test.py and False in - cms/envs/test.py. This implicitly gives us coverage of the full settings - mechanism with both values, so we do not have explicit test methods as they - are superfluous. - """ - - def test_can_enable_google_oauth2(self): - auth_settings.apply_settings({'Google': {'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': 'google_key'}}, settings) - self.assertEqual([provider.GoogleOauth2], provider.Registry.enabled()) - self.assertEqual('google_key', settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY) - - def test_can_enable_linkedin_oauth2(self): - auth_settings.apply_settings({'LinkedIn': {'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': 'linkedin_key'}}, settings) - self.assertEqual([provider.LinkedInOauth2], provider.Registry.enabled()) - self.assertEqual('linkedin_key', settings.SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY) diff --git a/common/djangoapps/third_party_auth/tests/test_views.py b/common/djangoapps/third_party_auth/tests/test_views.py new file mode 100644 index 0000000000..8e88629801 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/test_views.py @@ -0,0 +1,64 @@ +""" +Test the views served by third_party_auth. +""" +# pylint: disable=no-member +import ddt +from lxml import etree +import unittest +from .testutil import AUTH_FEATURE_ENABLED, SAMLTestCase + +# Define some XML namespaces: +from third_party_auth.tasks import SAML_XML_NS +XMLDSIG_XML_NS = 'http://www.w3.org/2000/09/xmldsig#' + + +@unittest.skipUnless(AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') +@ddt.ddt +class SAMLMetadataTest(SAMLTestCase): + """ + Test the SAML metadata view + """ + METADATA_URL = '/auth/saml/metadata.xml' + + def test_saml_disabled(self): + """ When SAML is not enabled, the metadata view should return 404 """ + self.enable_saml(enabled=False) + response = self.client.get(self.METADATA_URL) + self.assertEqual(response.status_code, 404) + + @ddt.data('saml_key', 'saml_key_alt') # Test two slightly different key pair export formats + def test_metadata(self, key_name): + self.enable_saml( + private_key=self._get_private_key(key_name), + public_key=self._get_public_key(key_name), + entity_id="https://saml.example.none", + ) + doc = self._fetch_metadata() + # Check the ACS URL: + acs_node = doc.find(".//{}".format(etree.QName(SAML_XML_NS, 'AssertionConsumerService'))) + self.assertIsNotNone(acs_node) + self.assertEqual(acs_node.attrib['Location'], 'http://example.none/auth/complete/tpa-saml/') + + def test_signed_metadata(self): + self.enable_saml( + private_key=self._get_private_key(), + public_key=self._get_public_key(), + entity_id="https://saml.example.none", + other_config_str='{"SECURITY_CONFIG": {"signMetadata": true} }', + ) + doc = self._fetch_metadata() + sig_node = doc.find(".//{}".format(etree.QName(XMLDSIG_XML_NS, 'SignatureValue'))) + self.assertIsNotNone(sig_node) + + def _fetch_metadata(self): + """ Fetch and parse the metadata XML at self.METADATA_URL """ + response = self.client.get(self.METADATA_URL) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'text/xml') + # The result should be valid XML: + try: + metadata_doc = etree.fromstring(response.content) + except etree.LxmlError: + self.fail('SAML metadata must be valid XML') + self.assertEqual(metadata_doc.tag, etree.QName(SAML_XML_NS, 'EntityDescriptor')) + return metadata_doc diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py index eb3f84e5e6..5d1a1f38c2 100644 --- a/common/djangoapps/third_party_auth/tests/testutil.py +++ b/common/djangoapps/third_party_auth/tests/testutil.py @@ -5,13 +5,16 @@ Used by Django and non-Django tests; must not have Django deps. """ from contextlib import contextmanager -import unittest +from django.conf import settings +import django.test import mock +import os.path -from third_party_auth import provider +from third_party_auth.models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, cache as config_cache AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' +AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES class FakeDjangoSettings(object): @@ -23,22 +26,93 @@ class FakeDjangoSettings(object): setattr(self, key, value) -class TestCase(unittest.TestCase): - """Base class for auth test cases.""" - - # Allow access to protected methods (or module-protected methods) under - # test. - # pylint: disable-msg=protected-access - - def setUp(self): - super(TestCase, self).setUp() - self._original_providers = provider.Registry._get_all() - provider.Registry._reset() +class ThirdPartyAuthTestMixin(object): + """ Helper methods useful for testing third party auth functionality """ def tearDown(self): - provider.Registry._reset() - provider.Registry.configure_once(self._original_providers) - super(TestCase, self).tearDown() + config_cache.clear() + super(ThirdPartyAuthTestMixin, self).tearDown() + + def enable_saml(self, **kwargs): + """ Enable SAML support (via SAMLConfiguration, not for any particular provider) """ + kwargs.setdefault('enabled', True) + SAMLConfiguration(**kwargs).save() + + @staticmethod + def configure_oauth_provider(**kwargs): + """ Update the settings for an OAuth2-based third party auth provider """ + obj = OAuth2ProviderConfig(**kwargs) + obj.save() + return obj + + def configure_saml_provider(self, **kwargs): + """ Update the settings for a SAML-based third party auth provider """ + self.assertTrue(SAMLConfiguration.is_enabled(), "SAML Provider Configuration only works if SAML is enabled.") + obj = SAMLProviderConfig(**kwargs) + obj.save() + return obj + + @classmethod + def configure_google_provider(cls, **kwargs): + """ Update the settings for the Google third party auth provider/backend """ + kwargs.setdefault("name", "Google") + kwargs.setdefault("backend_name", "google-oauth2") + kwargs.setdefault("icon_class", "fa-google-plus") + kwargs.setdefault("key", "test-fake-key.apps.googleusercontent.com") + kwargs.setdefault("secret", "opensesame") + return cls.configure_oauth_provider(**kwargs) + + @classmethod + def configure_facebook_provider(cls, **kwargs): + """ Update the settings for the Facebook third party auth provider/backend """ + kwargs.setdefault("name", "Facebook") + kwargs.setdefault("backend_name", "facebook") + kwargs.setdefault("icon_class", "fa-facebook") + kwargs.setdefault("key", "FB_TEST_APP") + kwargs.setdefault("secret", "opensesame") + return cls.configure_oauth_provider(**kwargs) + + @classmethod + def configure_linkedin_provider(cls, **kwargs): + """ Update the settings for the LinkedIn third party auth provider/backend """ + kwargs.setdefault("name", "LinkedIn") + kwargs.setdefault("backend_name", "linkedin-oauth2") + kwargs.setdefault("icon_class", "fa-linkedin") + kwargs.setdefault("key", "test") + kwargs.setdefault("secret", "test") + return cls.configure_oauth_provider(**kwargs) + + +class TestCase(ThirdPartyAuthTestMixin, django.test.TestCase): + """Base class for auth test cases.""" + pass + + +class SAMLTestCase(TestCase): + """ + Base class for SAML-related third_party_auth tests + """ + + def setUp(self): + super(SAMLTestCase, self).setUp() + self.client.defaults['SERVER_NAME'] = 'example.none' # The SAML lib we use doesn't like testserver' as a domain + self.url_prefix = 'http://example.none' + + @classmethod + def _get_public_key(cls, key_name='saml_key'): + """ Get a public key for use in the test. """ + return cls._read_data_file('{}.pub'.format(key_name)) + + @classmethod + def _get_private_key(cls, key_name='saml_key'): + """ Get a private key for use in the test. """ + return cls._read_data_file('{}.key'.format(key_name)) + + @staticmethod + def _read_data_file(filename): + """ Read the contents of a file in the data folder """ + with open(os.path.join(os.path.dirname(__file__), 'data', filename)) as f: + return f.read() @contextmanager diff --git a/common/djangoapps/third_party_auth/tests/utils.py b/common/djangoapps/third_party_auth/tests/utils.py index 208930cdf4..cce2edd59b 100644 --- a/common/djangoapps/third_party_auth/tests/utils.py +++ b/common/djangoapps/third_party_auth/tests/utils.py @@ -9,9 +9,11 @@ from social.apps.django_app.default.models import UserSocialAuth from student.tests.factories import UserFactory +from .testutil import ThirdPartyAuthTestMixin + @httpretty.activate -class ThirdPartyOAuthTestMixin(object): +class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin): """ Mixin with tests for third party oauth views. A TestCase that includes this must define the following: @@ -32,6 +34,10 @@ class ThirdPartyOAuthTestMixin(object): if create_user: self.user = UserFactory() UserSocialAuth.objects.create(user=self.user, provider=self.BACKEND, uid=self.social_uid) + if self.BACKEND == 'google-oauth2': + self.configure_google_provider(enabled=True) + elif self.BACKEND == 'facebook': + self.configure_facebook_provider(enabled=True) def _setup_provider_response(self, success=False, email=''): """ diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py index b020e775b5..5d366b2da3 100644 --- a/common/djangoapps/third_party_auth/urls.py +++ b/common/djangoapps/third_party_auth/urls.py @@ -2,10 +2,11 @@ from django.conf.urls import include, patterns, url -from .views import inactive_user_view +from .views import inactive_user_view, saml_metadata_view urlpatterns = patterns( '', url(r'^auth/inactive', inactive_user_view), + url(r'^auth/saml/metadata.xml', saml_metadata_view), url(r'^auth/', include('social.apps.django_app.urls', namespace='social')), ) diff --git a/common/djangoapps/third_party_auth/views.py b/common/djangoapps/third_party_auth/views.py index 5ae69db526..ef0233f33c 100644 --- a/common/djangoapps/third_party_auth/views.py +++ b/common/djangoapps/third_party_auth/views.py @@ -1,7 +1,12 @@ """ Extra views required for SSO """ +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseServerError, Http404 from django.shortcuts import redirect +from social.apps.django_app.utils import load_strategy, load_backend +from .models import SAMLConfiguration def inactive_user_view(request): @@ -13,3 +18,21 @@ def inactive_user_view(request): # in a course. Otherwise, just redirect them to the dashboard, which displays a message # about activating their account. return redirect(request.GET.get('next', 'dashboard')) + + +def saml_metadata_view(request): + """ + Get the Service Provider metadata for this edx-platform instance. + You must send this XML to any Shibboleth Identity Provider that you wish to use. + """ + if not SAMLConfiguration.is_enabled(): + raise Http404 + complete_url = reverse('social:complete', args=("tpa-saml", )) + if settings.APPEND_SLASH and not complete_url.endswith('/'): + complete_url = complete_url + '/' # Required for consistency + saml_backend = load_backend(load_strategy(request), "tpa-saml", redirect_uri=complete_url) + metadata, errors = saml_backend.generate_metadata_xml() + + if not errors: + return HttpResponse(content=metadata, content_type='text/xml') + return HttpResponseServerError(content=', '.join(errors)) diff --git a/common/djangoapps/util/date_utils.py b/common/djangoapps/util/date_utils.py index 11ca25d087..e9c4f905d3 100644 --- a/common/djangoapps/util/date_utils.py +++ b/common/djangoapps/util/date_utils.py @@ -2,7 +2,7 @@ Convenience methods for working with datetime objects """ -from datetime import timedelta +from datetime import datetime, timedelta import re from pytz import timezone, UTC, UnknownTimeZoneError @@ -73,6 +73,27 @@ def almost_same_datetime(dt1, dt2, allowed_delta=timedelta(minutes=1)): return abs(dt1 - dt2) < allowed_delta +def to_timestamp(datetime_value): + """ + Convert a datetime into a timestamp, represented as the number + of seconds since January 1, 1970 UTC. + """ + return int((datetime_value - datetime(1970, 1, 1, tzinfo=UTC)).total_seconds()) + + +def from_timestamp(timestamp): + """ + Convert a timestamp (number of seconds since Jan 1, 1970 UTC) + into a timezone-aware datetime. + + If the timestamp cannot be converted, returns None instead. + """ + try: + return datetime.utcfromtimestamp(int(timestamp)).replace(tzinfo=UTC) + except (ValueError, TypeError): + return None + + DEFAULT_SHORT_DATE_FORMAT = "%b %d, %Y" DEFAULT_LONG_DATE_FORMAT = "%A, %B %d, %Y" DEFAULT_TIME_FORMAT = "%I:%M:%S %p" diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index f4ea46932d..c487efed69 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -69,7 +69,7 @@ registry = TagRegistry() # pylint: disable=invalid-name class Status(object): """ Problem status - attributes: classname, display_name + attributes: classname, display_name, display_tooltip """ css_classes = { # status: css class @@ -77,7 +77,7 @@ class Status(object): 'incomplete': 'incorrect', 'queued': 'processing', } - __slots__ = ('classname', '_status', 'display_name') + __slots__ = ('classname', '_status', 'display_name', 'display_tooltip') def __init__(self, status, gettext_func=unicode): self.classname = self.css_classes.get(status, status) @@ -90,7 +90,16 @@ class Status(object): 'unsubmitted': _('unanswered'), 'queued': _('processing'), } + tooltips = { + # Translators: these are tooltips that indicate the state of an assessment question + 'correct': _('This is correct.'), + 'incorrect': _('This is incorrect.'), + 'unanswered': _('This is unanswered.'), + 'unsubmitted': _('This is unanswered.'), + 'queued': _('This is being processed.'), + } self.display_name = names.get(status, unicode(status)) + self.display_tooltip = tooltips.get(status, u'') self._status = status or '' def __str__(self): diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index 814fae6594..da5d372401 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -1,24 +1,5 @@ -
      - % if input_type == 'checkbox' or not value: - - - %for choice_id, choice_description in choices: - % if choice_id in value: - ${choice_description}, - %endif - %endfor - - - ${status.display_name} - - - % endif -
      -
      - % for choice_id, choice_description in choices:
      - +
      + % if input_type == 'checkbox' or not value: + + + %for choice_id, choice_description in choices: + % if choice_id in value: + ${choice_description}, + %endif + %endfor + - + ${status.display_name} + + + % endif +
      % if show_correctness == "never" and (value or status not in ['unsubmitted']):
      ${submitted_message}
      %endif diff --git a/common/lib/capa/capa/templates/choicetext.html b/common/lib/capa/capa/templates/choicetext.html index 91a894dd4f..448205c2aa 100644 --- a/common/lib/capa/capa/templates/choicetext.html +++ b/common/lib/capa/capa/templates/choicetext.html @@ -9,12 +9,7 @@
      -
      - % if input_type == 'checkbox' or not element_checked: - - % endif -
      - +
      % for choice_id, choice_description in choices: <%choice_id= choice_id %> @@ -62,6 +57,13 @@
      + +
      + % if input_type == 'checkbox' or not element_checked: + + % endif +
      + % if show_correctness == "never" and (value or status not in ['unsubmitted']):
      ${_(submitted_message)}
      %endif diff --git a/common/lib/capa/capa/templates/formulaequationinput.html b/common/lib/capa/capa/templates/formulaequationinput.html index de56fd5098..206a8bc21c 100644 --- a/common/lib/capa/capa/templates/formulaequationinput.html +++ b/common/lib/capa/capa/templates/formulaequationinput.html @@ -10,9 +10,11 @@ % endif /> -

      - ${status.display_name} -

      + + + ${status.display_name} + +
      \[\] diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 9e093bac43..ee8cc62c58 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -49,9 +49,8 @@
      %endif -
      diff --git a/common/lib/capa/capa/templates/optioninput.html b/common/lib/capa/capa/templates/optioninput.html index f804e771cc..a4f25d801b 100644 --- a/common/lib/capa/capa/templates/optioninput.html +++ b/common/lib/capa/capa/templates/optioninput.html @@ -13,12 +13,13 @@ - - ${value|h} - ${status.display_name} - - +
      + + ${value|h} - ${status.display_name} + +
      % if msg: ${msg|n} % endif diff --git a/common/lib/capa/capa/templates/textline.html b/common/lib/capa/capa/templates/textline.html index 8a9826b0a5..b37fb0d67e 100644 --- a/common/lib/capa/capa/templates/textline.html +++ b/common/lib/capa/capa/templates/textline.html @@ -27,18 +27,20 @@ /> ${trailing_text | h} -

      - %if value: - ${value|h} - % else: - ${label} - %endif - - - ${status.display_name} -

      + aria-describedby="input_${id}" data-tooltip="${status.display_tooltip}"> + + %if value: + ${value|h} + % else: + ${label} + %endif + - + ${status.display_name} + +

      diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 09ae25476d..5af21fd5e2 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -144,7 +144,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): # Should mark the entire problem correct xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='status correct']" + xpath = "//div[@class='indicator-container']/span[@class='status correct']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -172,7 +172,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='status incorrect']" + xpath = "//div[@class='indicator-container']/span[@class='status incorrect']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -204,7 +204,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='status unanswered']" + xpath = "//div[@class='indicator-container']/span[@class='status unanswered']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -234,7 +234,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark the whole problem - xpath = "//div[@class='indicator_container']/span" + xpath = "//div[@class='indicator-container']/span" self.assert_no_xpath(xml, xpath, self.context) def test_option_marked_incorrect(self): @@ -255,7 +255,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark the whole problem - xpath = "//div[@class='indicator_container']/span" + xpath = "//div[@class='indicator-container']/span" self.assert_no_xpath(xml, xpath, self.context) def test_never_show_correctness(self): @@ -289,10 +289,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): xml = self.render_to_xml(self.context) # Should NOT mark the entire problem correct/incorrect - xpath = "//div[@class='indicator_container']/span[@class='status correct']" + xpath = "//div[@class='indicator-container']/span[@class='status correct']" self.assert_no_xpath(xml, xpath, self.context) - xpath = "//div[@class='indicator_container']/span[@class='status incorrect']" + xpath = "//div[@class='indicator-container']/span[@class='status incorrect']" self.assert_no_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -388,9 +388,9 @@ class TextlineTemplateTest(TemplateTestCase): xpath = "//div[@class='%s ']" % div_class self.assert_has_xpath(xml, xpath, self.context) - # Expect that we get a

      with class="status" + # Expect that we get a with class="status" # (used to by CSS to draw the green check / red x) - self.assert_has_text(xml, "//p[@class='status']", + self.assert_has_text(xml, "//span[@class='status']/span[@class='sr']", status_mark, exact=False) def test_label(self): @@ -848,7 +848,7 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): # Should mark the entire problem correct xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='status correct']" + xpath = "//div[@class='indicator-container']/span[@class='status correct']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -875,7 +875,7 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='status incorrect']" + xpath = "//div[@class='indicator-container']/span[@class='status incorrect']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -907,7 +907,7 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='status unanswered']" + xpath = "//div[@class='indicator-container']/span[@class='status unanswered']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -937,7 +937,7 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark the whole problem - xpath = "//div[@class='indicator_container']/span" + xpath = "//div[@class='indicator-container']/span" self.assert_no_xpath(xml, xpath, self.context) def test_option_marked_incorrect(self): @@ -957,7 +957,7 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark the whole problem - xpath = "//div[@class='indicator_container']/span" + xpath = "//div[@class='indicator-container']/span" self.assert_no_xpath(xml, xpath, self.context) def test_label(self): diff --git a/common/lib/safe_lxml/safe_lxml/etree.py b/common/lib/safe_lxml/safe_lxml/etree.py index 83052b22b6..97bc0b7547 100644 --- a/common/lib/safe_lxml/safe_lxml/etree.py +++ b/common/lib/safe_lxml/safe_lxml/etree.py @@ -9,7 +9,7 @@ For processing xml always prefer this over using lxml.etree directly. from lxml.etree import * # pylint: disable=wildcard-import, unused-wildcard-import from lxml.etree import XMLParser as _XMLParser -from lxml.etree import _ElementTree # pylint: disable=unused-import +from lxml.etree import _Element, _ElementTree # pylint: disable=unused-import, no-name-in-module # This should be imported after lxml.etree so that it overrides the following attributes. from defusedxml.lxml import parse, fromstring, XML diff --git a/common/lib/xmodule/xmodule/course_metadata_utils.py b/common/lib/xmodule/xmodule/course_metadata_utils.py new file mode 100644 index 0000000000..832be14d1a --- /dev/null +++ b/common/lib/xmodule/xmodule/course_metadata_utils.py @@ -0,0 +1,211 @@ +""" +Simple utility functions that operate on course metadata. + +This is a place to put simple functions that operate on course metadata. It +allows us to share code between the CourseDescriptor and CourseOverview +classes, which both need these type of functions. +""" +from datetime import datetime +from base64 import b32encode + +from django.utils.timezone import UTC + +from .fields import Date + +DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=UTC()) + + +def clean_course_key(course_key, padding_char): + """ + Encode a course's key into a unique, deterministic base32-encoded ID for + the course. + + Arguments: + course_key (CourseKey): A course key. + padding_char (str): Character used for padding at end of the encoded + string. The standard value for this is '='. + """ + return "course_{}".format( + b32encode(unicode(course_key)).replace('=', padding_char) + ) + + +def url_name_for_course_location(location): + """ + Given a course's usage locator, returns the course's URL name. + + Arguments: + location (BlockUsageLocator): The course's usage locator. + """ + return location.name + + +def display_name_with_default(course): + """ + Calculates the display name for a course. + + Default to the display_name if it isn't None, else fall back to creating + a name based on the URL. + + Unlike the rest of this module's functions, this function takes an entire + course descriptor/overview as a parameter. This is because a few test cases + (specifically, {Text|Image|Video}AnnotationModuleTestCase.test_student_view) + create scenarios where course.display_name is not None but course.location + is None, which causes calling course.url_name to fail. So, although we'd + like to just pass course.display_name and course.url_name as arguments to + this function, we can't do so without breaking those tests. + + Arguments: + course (CourseDescriptor|CourseOverview): descriptor or overview of + said course. + """ + # TODO: Consider changing this to use something like xml.sax.saxutils.escape + return ( + course.display_name if course.display_name is not None + else course.url_name.replace('_', ' ') + ).replace('<', '<').replace('>', '>') + + +def number_for_course_location(location): + """ + Given a course's block usage locator, returns the course's number. + + This is a "number" in the sense of the "course numbers" that you see at + lots of universities. For example, given a course + "Intro to Computer Science" with the course key "edX/CS-101/2014", the + course number would be "CS-101" + + Arguments: + location (BlockUsageLocator): The usage locator of the course in + question. + """ + return location.course + + +def has_course_started(start_date): + """ + Given a course's start datetime, returns whether the current time's past it. + + Arguments: + start_date (datetime): The start datetime of the course in question. + """ + # TODO: This will throw if start_date is None... consider changing this behavior? + return datetime.now(UTC()) > start_date + + +def has_course_ended(end_date): + """ + Given a course's end datetime, returns whether + (a) it is not None, and + (b) the current time is past it. + + Arguments: + end_date (datetime): The end datetime of the course in question. + """ + return datetime.now(UTC()) > end_date if end_date is not None else False + + +def course_start_date_is_default(start, advertised_start): + """ + Returns whether a course's start date hasn't yet been set. + + Arguments: + start (datetime): The start datetime of the course in question. + advertised_start (str): The advertised start date of the course + in question. + """ + return advertised_start is None and start == DEFAULT_START_DATE + + +def _datetime_to_string(date_time, format_string, strftime_localized): + """ + Formats the given datetime with the given function and format string. + + Adds UTC to the resulting string if the format is DATE_TIME or TIME. + + Arguments: + date_time (datetime): the datetime to be formatted + format_string (str): the date format type, as passed to strftime + strftime_localized ((datetime, str) -> str): a nm localized string + formatting function + """ + # TODO: Is manually appending UTC really the right thing to do here? What if date_time isn't UTC? + result = strftime_localized(date_time, format_string) + return ( + result + u" UTC" if format_string in ['DATE_TIME', 'TIME'] + else result + ) + + +def course_start_datetime_text(start_date, advertised_start, format_string, ugettext, strftime_localized): + """ + Calculates text to be shown to user regarding a course's start + datetime in UTC. + + Prefers .advertised_start, then falls back to .start. + + Arguments: + start_date (datetime): the course's start datetime + advertised_start (str): the course's advertised start date + format_string (str): the date format type, as passed to strftime + ugettext ((str) -> str): a text localization function + strftime_localized ((datetime, str) -> str): a localized string + formatting function + """ + if advertised_start is not None: + # TODO: This will return an empty string if advertised_start == ""... consider changing this behavior? + try: + # from_json either returns a Date, returns None, or raises a ValueError + parsed_advertised_start = Date().from_json(advertised_start) + if parsed_advertised_start is not None: + # In the Django implementation of strftime_localized, if + # the year is <1900, _datetime_to_string will raise a ValueError. + return _datetime_to_string(parsed_advertised_start, format_string, strftime_localized) + except ValueError: + pass + return advertised_start.title() + elif start_date != DEFAULT_START_DATE: + return _datetime_to_string(start_date, format_string, strftime_localized) + else: + _ = ugettext + # Translators: TBD stands for 'To Be Determined' and is used when a course + # does not yet have an announced start date. + return _('TBD') + + +def course_end_datetime_text(end_date, format_string, strftime_localized): + """ + Returns a formatted string for a course's end date or datetime. + + If end_date is None, an empty string will be returned. + + Arguments: + end_date (datetime): the end datetime of a course + format_string (str): the date format type, as passed to strftime + strftime_localized ((datetime, str) -> str): a localized string + formatting function + """ + return ( + _datetime_to_string(end_date, format_string, strftime_localized) if end_date is not None + else '' + ) + + +def may_certify_for_course(certificates_display_behavior, certificates_show_before_end, has_ended): + """ + Returns whether it is acceptable to show the student a certificate download + link for a course. + + Arguments: + certificates_display_behavior (str): string describing the course's + certificate display behavior. + See CourseFields.certificates_display_behavior.help for more detail. + certificates_show_before_end (bool): whether user can download the + course's certificates before the course has ended. + has_ended (bool): Whether the course has ended. + """ + show_early = ( + certificates_display_behavior in ('early_with_info', 'early_no_info') + or certificates_show_before_end + ) + return show_early or has_ended diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index a0334fdd89..5a5066c344 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -10,8 +10,9 @@ import requests from datetime import datetime import dateutil.parser from lazy import lazy -from base64 import b32encode +from xmodule import course_metadata_utils +from xmodule.course_metadata_utils import DEFAULT_START_DATE from xmodule.exceptions import UndefinedContext from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.graders import grader_from_conf @@ -29,8 +30,6 @@ log = logging.getLogger(__name__) # Make '_' a no-op so we can scrape strings _ = lambda text: text -DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=UTC()) - CATALOG_VISIBILITY_CATALOG_AND_ABOUT = "both" CATALOG_VISIBILITY_ABOUT = "about" CATALOG_VISIBILITY_NONE = "none" @@ -920,7 +919,7 @@ class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-me """ -class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): +class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): """ The descriptor for the course XModule """ @@ -1089,20 +1088,20 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): Returns True if the current time is after the specified course end date. Returns False if there is no end date specified. """ - if self.end is None: - return False - - return datetime.now(UTC()) > self.end + return course_metadata_utils.has_course_ended(self.end) def may_certify(self): """ - Return True if it is acceptable to show the student a certificate download link + Return whether it is acceptable to show the student a certificate download link. """ - show_early = self.certificates_display_behavior in ('early_with_info', 'early_no_info') or self.certificates_show_before_end - return show_early or self.has_ended() + return course_metadata_utils.may_certify_for_course( + self.certificates_display_behavior, + self.certificates_show_before_end, + self.has_ended() + ) def has_started(self): - return datetime.now(UTC()) > self.start + return course_metadata_utils.has_course_started(self.start) @property def grader(self): @@ -1361,36 +1360,13 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): then falls back to .start """ i18n = self.runtime.service(self, "i18n") - _ = i18n.ugettext - strftime = i18n.strftime - - def try_parse_iso_8601(text): - try: - result = Date().from_json(text) - if result is None: - result = text.title() - else: - result = strftime(result, format_string) - if format_string == "DATE_TIME": - result = self._add_timezone_string(result) - except ValueError: - result = text.title() - - return result - - if isinstance(self.advertised_start, basestring): - return try_parse_iso_8601(self.advertised_start) - elif self.start_date_is_still_default: - # Translators: TBD stands for 'To Be Determined' and is used when a course - # does not yet have an announced start date. - return _('TBD') - else: - when = self.advertised_start or self.start - - if format_string == "DATE_TIME": - return self._add_timezone_string(strftime(when, format_string)) - - return strftime(when, format_string) + return course_metadata_utils.course_start_datetime_text( + self.start, + self.advertised_start, + format_string, + i18n.ugettext, + i18n.strftime + ) @property def start_date_is_still_default(self): @@ -1398,26 +1374,20 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): Checks if the start date set for the course is still default, i.e. .start has not been modified, and .advertised_start has not been set. """ - return self.advertised_start is None and self.start == CourseFields.start.default + return course_metadata_utils.course_start_date_is_default( + self.start, + self.advertised_start + ) def end_datetime_text(self, format_string="SHORT_DATE"): """ Returns the end date or date_time for the course formatted as a string. - - If the course does not have an end date set (course.end is None), an empty string will be returned. """ - if self.end is None: - return '' - else: - strftime = self.runtime.service(self, "i18n").strftime - date_time = strftime(self.end, format_string) - return date_time if format_string == "SHORT_DATE" else self._add_timezone_string(date_time) - - def _add_timezone_string(self, date_time): - """ - Adds 'UTC' string to the end of start/end date and time texts. - """ - return date_time + u" UTC" + return course_metadata_utils.course_end_datetime_text( + self.end, + format_string, + self.runtime.service(self, "i18n").strftime + ) def get_discussion_blackout_datetimes(self): """ @@ -1458,7 +1428,15 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): @property def number(self): - return self.location.course + """ + Returns this course's number. + + This is a "number" in the sense of the "course numbers" that you see at + lots of universities. For example, given a course + "Intro to Computer Science" with the course key "edX/CS-101/2014", the + course number would be "CS-101" + """ + return course_metadata_utils.number_for_course_location(self.location) @property def display_number_with_default(self): @@ -1499,9 +1477,7 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): Returns a unique deterministic base32-encoded ID for the course. The optional padding_char parameter allows you to override the "=" character used for padding. """ - return "course_{}".format( - b32encode(unicode(self.location.course_key)).replace('=', padding_char) - ) + return course_metadata_utils.clean_course_key(self.location.course_key, padding_char) @property def teams_enabled(self): diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 727630b79b..e29d186286 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -1,8 +1,55 @@ +// capa - styling +// ==================== + +// Table of Contents +// * +Variables - Capa +// * +Extends - Capa +// * +Mixins - Status Icon - Capa +// * +Resets - Deprecate Please +// * +Problem - Base +// * +Problem - Choice Group +// * +Problem - Misc, Unclassified Mess +// * +Problem - Text Input, Numerical Input +// * +Problem - Option Input (Dropdown) +// * +Problem - CodeMirror +// * +Problem - Misc, Unclassified Mess Part 2 +// * +Problem - Rubric +// * +Problem - Annotation +// * +Problem - Choice Text Group +// * +Problem - Image Input Overrides + +// +Variables - Capa +// ==================== $annotation-yellow: rgba(255,255,10,0.3); $color-copy-tip: rgb(100,100,100); -$color-success: rgb(0, 136, 1); -$color-fail: rgb(212, 64, 64); +$correct: $green-d1; +$incorrect: $red; +// +Extends - Capa +// ==================== +// Duplicated from _mixins.scss due to xmodule compilation, inheritance issues +%use-font-awesome { + font-family: FontAwesome; + -webkit-font-smoothing: antialiased; + display: inline-block; + speak: none; +} + +// +Mixins - Status Icon - Capa +// ==================== +@mixin status-icon($color: $gray, $fontAwesomeIcon: "\f00d"){ + + &:after { + @extend %use-font-awesome; + @include margin-left(17px); + color: $color; + font-size: 1.2em; + content: $fontAwesomeIcon; + } +} + +// +Resets - Deprecate Please +// ==================== h2 { margin-top: 0; margin-bottom: ($baseline*0.75); @@ -24,12 +71,12 @@ h2 { .feedback-hint-correct { margin-top: ($baseline/2); - color: $color-success; + color: $correct; } .feedback-hint-incorrect { margin-top: ($baseline/2); - color: $color-fail; + color: $incorrect; } .feedback-hint-text { @@ -55,10 +102,9 @@ h2 { display: block; } - iframe[seamless]{ overflow: hidden; - padding: 0px; + padding: 0; border: 0px none transparent; background-color: transparent; } @@ -68,13 +114,16 @@ iframe[seamless]{ } div.problem-progress { + @include padding-left($baseline/4); + @extend %t-ultralight; display: inline-block; - padding-left: ($baseline/4); - color: #666; + color: $gray-d1; font-weight: 100; font-size: em(16); } +// +Problem - Base +// ==================== div.problem { @media print { display: block; @@ -89,7 +138,11 @@ div.problem { .inline { display: inline; } +} +// +Problem - Choice Group +// ==================== +div.problem { .choicegroup { @include clearfix(); min-width: 100px; @@ -97,51 +150,98 @@ div.problem { width: 100px; label { - @include float(left); + @include box-sizing(border-box); + display: inline-block; clear: both; - margin-bottom: ($baseline/4); + margin-bottom: ($baseline/2); + border: 2px solid $gray-l4; + border-radius: 3px; + padding: ($baseline/2); + width: 100%; &.choicegroup_correct { - &:after { - margin-left: ($baseline*0.75); - content: url('../images/correct-icon.png'); + @include status-icon($correct, "\f00c"); + border: 2px solid $correct; + + // keep green for correct answers on hover. + &:hover { + border-color: $correct; } } &.choicegroup_incorrect { - &:after { - margin-left: ($baseline*0.75); - content: url('../images/incorrect-icon.png'); + @include status-icon($incorrect, "\f00d"); + border: 2px solid $incorrect; + + // keep red for incorrect answers on hover. + &:hover { + border-color: $incorrect; } } + + &:hover { + border: 2px solid $blue; + } } - .indicator_container { - @include float(left); + .indicator-container { + display: inline-block; + min-height: 1px; width: 25px; - height: 1px; - @include margin-right(15px); } fieldset { @include box-sizing(border-box); - margin: 0px 0px $baseline; - @include padding-left($baseline); - @include border-left(1px solid #ddd); } input[type="radio"], input[type="checkbox"] { - @include float(left); - @include margin(4px, 8px, 0, 0); + @include margin(($baseline/4) ($baseline/2) ($baseline/4) ($baseline/4)); } text { + @include margin-left(25px); display: inline; - margin-left: 25px; } } +} +// +Problem - Status Indicators +// ==================== +// Summary status indicators shown after the input area +div.problem { + + .indicator-container { + + .status { + width: $baseline; + height: $baseline; + + // CASE: correct answer + &.correct { + @include status-icon($correct, "\f00c"); + } + + // CASE: incorrect answer + &.incorrect { + @include status-icon($incorrect, "\f00d"); + } + + // CASE: unanswered + &.unanswered { + @include status-icon($gray-l4, "\f128"); + } + + // CASE: processing + &.processing { + } + } + } +} + +// +Problem - Misc, Unclassified Mess +// ==================== +div.problem { ol.enumerate { li { &:before { @@ -187,17 +287,22 @@ div.problem { } } + // known classes using this div: .indicator-container, moved to section above div { + + // TO-DO: Styling used by advanced capa problem types. Should be synced up to use .status class p { &.answer { margin-top: -2px; } + &.status { - margin: 8px 0 0 $baseline/2; + @include margin(8px, 0, 0, ($baseline/2)); text-indent: 100%; white-space: nowrap; overflow: hidden; } + span.clarification i { font-style: normal; &:hover { @@ -224,7 +329,7 @@ div.problem { } input { - border-color: green; + border-color: $correct; } } @@ -241,7 +346,7 @@ div.problem { } } - &.incorrect, &.incomplete, &.ui-icon-close { + &.ui-icon-close { p.status { display: inline-block; width: 20px; @@ -250,7 +355,21 @@ div.problem { } input { - border-color: red; + border-color: $incorrect; + } + } + + &.incorrect, &.incomplete { + + p.status { + display: inline-block; + width: 20px; + height: 20px; + background: url('../images/incorrect-icon.png') center center no-repeat; + } + + input { + border-color: $incorrect; } } @@ -260,14 +379,14 @@ div.problem { } p.answer { + @include margin-left($baseline/2); display: inline-block; margin-bottom: 0; - margin-left: $baseline/2; &:before { + @extend %t-strong; display: inline; content: "Answer: "; - font-weight: bold; } &:empty { @@ -287,8 +406,8 @@ div.problem { } img.loading { + @include padding-left($baseline/2); display: inline-block; - padding-left: ($baseline/2); } span { @@ -303,7 +422,7 @@ div.problem { background: #f1f1f1; } } - } + } // Hides equation previews in symbolic response problems when printing [id^='display'].equation { @@ -312,8 +431,9 @@ div.problem { } } + //TO-DO: review and deprecate all these styles within span {} span { - &.unanswered, &.ui-icon-bullet { + &.ui-icon-bullet { display: inline-block; position: relative; top: 4px; @@ -331,7 +451,7 @@ div.problem { background: url('../images/spinner.gif') center center no-repeat; } - &.correct, &.ui-icon-check { + &.ui-icon-check { display: inline-block; position: relative; top: 3px; @@ -349,7 +469,7 @@ div.problem { background: url('../images/partially-correct-icon.png') center center no-repeat; } - &.incorrect, &.incomplete, &.ui-icon-close { + &.incomplete, &.ui-icon-close { display: inline-block; position: relative; top: 3px; @@ -360,8 +480,8 @@ div.problem { } .reload { - float:right; - margin: $baseline/2; + @include float(right); + margin: ($baseline/2); } @@ -457,15 +577,6 @@ div.problem { } } - form.option-input { - margin: -$baseline/2 0 $baseline; - padding-bottom: $baseline; - - select { - margin-right: flex-gutter(); - } - } - ul { margin-bottom: lh(); margin-left: .75em; @@ -485,7 +596,8 @@ div.problem { } dl dt { - font-weight: bold; + @extend %t-strong; + } dl dd { @@ -531,8 +643,8 @@ div.problem { } th { + @extend %t-strong; text-align: left; - font-weight: bold; } td { @@ -584,6 +696,93 @@ div.problem { white-space: pre; } } +} + +// +Problem - Text Input, Numerical Input +// ==================== +.problem { + .capa_inputtype.textline, .inputtype.formulaequationinput { + + input { + @include box-sizing(border-box); + border: 2px solid $gray-l4; + border-radius: 3px; + min-width: 160px; + height: 46px; + } + + > .incorrect, .correct, .unanswered { + + .status { + display: inline-block; + margin-top: ($baseline/2); + background: none; + } + } + + // CASE: incorrect answer + > .incorrect { + + input { + border: 2px solid $incorrect; + } + + .status { + @include status-icon($incorrect, "\f00d"); + } + } + + // CASE: correct answer + > .correct { + + input { + border: 2px solid $correct; + } + + .status { + @include status-icon($correct, "\f00c"); + } + } + + // CASE: unanswered + > .unanswered { + + input { + border: 2px solid $gray-l4; + } + + .status { + @include status-icon($gray-l4, "\f128"); + } + } + } +} + + +// +Problem - Option Input (Dropdown) +// ==================== +.problem { + .inputtype.option-input { + margin: (-$baseline/2) 0 $baseline; + padding-bottom: $baseline; + + select { + @include margin-right($baseline/2); + } + + .indicator-container { + display: inline-block; + + .status.correct:after, .status.incorrect:after { + @include margin-left(0); + } + } + } +} + +// +Problem - CodeMirror +// ==================== +div.problem { .CodeMirror { border: 1px solid black; @@ -634,7 +833,52 @@ div.problem { .CodeMirror-scroll { margin-right: 0px; } +} +// +Problem - Actions +// ==================== +div.problem .action { + margin-top: $baseline; + + .save, .check, .show, .reset, .hint-button { + @include margin-right($baseline/2); + margin-bottom: ($baseline/2); + height: ($baseline*2); + vertical-align: middle; + text-transform: uppercase; + font-weight: 600; + } + + .save { + @extend .blue-button !optional; + } + + .show { + + .show-label { + font-weight: 600; + font-size: 1.0em; + } + } + + .submission_feedback { + // background: #F3F3F3; + // border: 1px solid #ddd; + // border-radius: 3px; + // padding: 8px 12px; + // margin-top: ($baseline/2); + @include margin-left($baseline/2); + display: inline-block; + margin-top: 8px; + color: $gray-d1; + font-style: italic; + -webkit-font-smoothing: antialiased; + } +} + +// +Problem - Misc, Unclassified Mess Part 2 +// ==================== +div.problem { hr { float: none; clear: both; @@ -663,52 +907,12 @@ div.problem { padding: lh(); border: 1px solid $gray-l3; } - - div.action { - margin-top: $baseline; - - .save, .check, .show, .reset, .hint-button { - height: ($baseline*2); - vertical-align: middle; - font-weight: 600; - - @media print { - display: none; - } - } - - .save { - @extend .blue-button !optional; - } - - .show { - - .show-label { - font-weight: 600; - font-size: 1.0em; - } - } - - .submission_feedback { - // background: #F3F3F3; - // border: 1px solid #ddd; - // border-radius: 3px; - // padding: 8px 12px; - // margin-top: ($baseline/2); - display: inline-block; - margin-top: 8px; - @include margin-left($baseline/2); - color: #666; - font-style: italic; - -webkit-font-smoothing: antialiased; - } - } - + .detailed-solution { > p:first-child { + @extend %t-strong; color: #aaa; text-transform: uppercase; - font-weight: bold; font-style: normal; font-size: 0.9em; } @@ -720,9 +924,9 @@ div.problem { .detailed-targeted-feedback { > p:first-child { - color: red; + @extend %t-strong; + color: $incorrect; text-transform: uppercase; - font-weight: bold; font-style: normal; font-size: 0.9em; } @@ -734,9 +938,9 @@ div.problem { .detailed-targeted-feedback-correct { > p:first-child { - color: green; + @extend %t-strong; + color: $correct; text-transform: uppercase; - font-weight: bold; font-style: normal; font-size: 0.9em; } @@ -777,11 +981,11 @@ div.problem { border: 1px solid $gray-l3; h3 { + @extend %t-strong; padding: 9px; border-bottom: 1px solid #e3e3e3; background: #eee; text-shadow: 0 1px 0 $white; - font-weight: bold; font-size: em(16); } @@ -818,9 +1022,9 @@ div.problem { margin-bottom: 12px; h3 { + @extend %t-strong; color: #aaa; text-transform: uppercase; - font-weight: bold; font-style: normal; font-size: 0.9em; } @@ -877,7 +1081,7 @@ div.problem { } .shortform { - font-weight: bold; + @extend %t-strong; } .longform { @@ -951,7 +1155,12 @@ div.problem { } } } +} + +// +Problem - Rubric +// ==================== +div.problem { .rubric { tr { margin: ($baseline/2) 0; @@ -1004,16 +1213,20 @@ div.problem { display: none; } } +} +// +Problem - Annotation +// ==================== +div.problem { .annotation-input { margin: 0 0 1em 0; border: 1px solid $gray-l3; border-radius: 1em; .annotation-header { + @extend %t-strong; padding: .5em 1em; border-bottom: 1px solid $gray-l3; - font-weight: bold; } .annotation-body { padding: .5em 1em; } @@ -1094,16 +1307,20 @@ div.problem { pre { background-color: $gray-l3; color: $black; } &:before { + @extend %t-strong; display: block; content: "debug input value"; text-transform: uppercase; - font-weight: bold; font-size: 1.5em; } } } +} - .choicetextgroup{ +// +Problem - Choice Text Group +// ==================== +div.problem { + .choicetextgroup { @extend .choicegroup; input[type="text"]{ @@ -1114,7 +1331,7 @@ div.problem { @extend label.choicegroup_correct; input[type="text"] { - border-color: green; + border-color: $correct; } } @@ -1134,3 +1351,24 @@ div.problem { } } } + +// +Problem - Image Input Overrides +// ==================== + +// NOTE: temporary override until image inputs use same base html structure as other common capa input types. +div.problem .imageinput.capa_inputtype { + + .status { + display: inline-block; + position: relative; + top: 3px; + width: 25px; + height: 20px; + } + .correct { + background: url('../images/correct-icon.png') center center no-repeat; + } + .incorrect { + background: url('../images/incorrect-icon.png') center center no-repeat; + } +} diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py index 066a1ad38b..11c0c28720 100644 --- a/common/lib/xmodule/xmodule/error_module.py +++ b/common/lib/xmodule/xmodule/error_module.py @@ -80,7 +80,18 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): return u'' @classmethod - def _construct(cls, system, contents, error_msg, location): + def _construct(cls, system, contents, error_msg, location, for_parent=None): + """ + Build a new ErrorDescriptor. using ``system``. + + Arguments: + system (:class:`DescriptorSystem`): The :class:`DescriptorSystem` used + to construct the XBlock that had an error. + contents (unicode): An encoding of the content of the xblock that had an error. + error_msg (unicode): A message describing the error. + location (:class:`UsageKey`): The usage key of the XBlock that had an error. + for_parent (:class:`XBlock`): Optional. The parent of this error block. + """ if error_msg is None: # this string is not marked for translation because we don't have @@ -110,6 +121,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): # real scope keys ScopeIds(None, 'error', location, location), field_data, + for_parent=for_parent, ) def get_context(self): @@ -139,6 +151,7 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): str(descriptor), error_msg, location=descriptor.location, + for_parent=descriptor.get_parent() if descriptor.has_cached_parent else None ) @classmethod diff --git a/common/lib/xmodule/xmodule/js/fixtures/matlabinput_problem.html b/common/lib/xmodule/xmodule/js/fixtures/matlabinput_problem.html new file mode 100644 index 0000000000..5dad82c727 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/matlabinput_problem.html @@ -0,0 +1,43 @@ +

      +
      +
      + +

      +

      +
      +
      + + +
      + + processing + + + + +

      processing

      +
      + + + +
      + Submitted. As soon as a response is returned, this message will be replaced by that feedback. +
      +
      + +
      + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee index 892864f631..170f0b3826 100644 --- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee @@ -323,7 +323,7 @@ describe 'Problem', ->

      -
      +
      @@ -628,3 +628,31 @@ describe 'Problem', -> it 'check_save_waitfor should return false', -> $(@problem.inputs[0]).data('waitfor', ->) expect(@problem.check_save_waitfor()).toEqual(false) + + describe 'Submitting an xqueue-graded problem', -> + matlabinput_html = readFixtures('matlabinput_problem.html') + + beforeEach -> + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> + callback html: matlabinput_html + jasmine.Clock.useMock() + @problem = new Problem($('.xblock-student_view')) + spyOn(@problem, 'poll').andCallThrough() + @problem.render(matlabinput_html) + + it 'check that we stop polling after a fixed amount of time', -> + expect(@problem.poll).not.toHaveBeenCalled() + jasmine.Clock.tick(1) + time_steps = [1000, 2000, 4000, 8000, 16000, 32000] + num_calls = 1 + for time_step in time_steps + do (time_step) => + jasmine.Clock.tick(time_step) + expect(@problem.poll.callCount).toEqual(num_calls) + num_calls += 1 + + # jump the next step and verify that we are not still continuing to poll + jasmine.Clock.tick(64000) + expect(@problem.poll.callCount).toEqual(6) + + expect($('.capa_alert').text()).toEqual("The grading process is still running. Refresh the page to see updates.") diff --git a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js index b22cdd375e..2f9126cd8c 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js @@ -51,14 +51,14 @@ }); }); - it('callback was called', function () { + it('callback was not called', function () { waitsFor(function () { return state.videoPlayer.player.getPlayerState() !== STATUS.PAUSED; }, 'Player state should be changed', WAIT_TIMEOUT); runs(function () { expect(state.videoPlayer.player.callStateChangeCallback) - .toHaveBeenCalled(); + .not.toHaveBeenCalled(); }); }); }); @@ -85,14 +85,14 @@ }); }); - it('callback was called', function () { + it('callback was not called', function () { waitsFor(function () { return state.videoPlayer.player.getPlayerState() !== STATUS.PLAYING; }, 'Player state should be changed', WAIT_TIMEOUT); runs(function () { expect(state.videoPlayer.player.callStateChangeCallback) - .toHaveBeenCalled(); + .not.toHaveBeenCalled(); }); }); }); diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 139654e5e5..43de29bfe4 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -98,19 +98,11 @@ class @Problem if @num_queued_items > 0 if window.queuePollerID # Only one poller 'thread' per Problem window.clearTimeout(window.queuePollerID) - queuelen = @get_queuelen() - window.queuePollerID = window.setTimeout(@poll, queuelen*10) + window.queuePollerID = window.setTimeout( + => @poll(1000), + 1000) - # Retrieves the minimum queue length of all queued items - get_queuelen: => - minlen = Infinity - @queued_items.each (index, qitem) -> - len = parseInt($.text(qitem)) - if len < minlen - minlen = len - return minlen - - poll: => + poll: (prev_timeout) => $.postWithPrefix "#{@url}/problem_get", (response) => # If queueing status changed, then render @new_queued_items = $(response.html).find(".xqueue") @@ -125,8 +117,16 @@ class @Problem @forceUpdate response delete window.queuePollerID else - # TODO: Some logic to dynamically adjust polling rate based on queuelen - window.queuePollerID = window.setTimeout(@poll, 1000) + new_timeout = prev_timeout * 2 + # if the timeout is greather than 1 minute + if new_timeout >= 60000 + delete window.queuePollerID + @gentle_alert gettext("The grading process is still running. Refresh the page to see updates.") + else + window.queuePollerID = window.setTimeout( + => @poll(new_timeout), + new_timeout + ) # Use this if you want to make an ajax call on the input type object @@ -468,9 +468,9 @@ class @Problem # They should set handlers on each to reset the whole. formulaequationinput: (element) -> $(element).find('input').on 'input', -> - $p = $(element).find('p.status') + $p = $(element).find('span.status') `// Translators: the word unanswered here is about answering a problem the student must solve.` - $p.parent().removeClass().addClass "unanswered" + $p.parent().removeClass().addClass "unsubmitted" choicegroup: (element) -> $element = $(element) @@ -496,9 +496,9 @@ class @Problem textline: (element) -> $(element).find('input').on 'input', -> - $p = $(element).find('p.status') + $p = $(element).find('span.status') `// Translators: the word unanswered here is about answering a problem the student must solve.` - $p.parent().removeClass("correct incorrect").addClass "unanswered" + $p.parent().removeClass("correct incorrect").addClass "unsubmitted" inputtypeSetupMethods: diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js index dc3fd7974b..dca85c9d85 100644 --- a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js @@ -290,13 +290,11 @@ function () { var PlayerState = HTML5Video.PlayerState; if (_this.playerState === PlayerState.PLAYING) { - _this.pauseVideo(); _this.playerState = PlayerState.PAUSED; - _this.callStateChangeCallback(); + _this.pauseVideo(); } else { - _this.playVideo(); _this.playerState = PlayerState.PLAYING; - _this.callStateChangeCallback(); + _this.playVideo(); } }); diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index f47c901b65..9d997cc2a5 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -272,12 +272,20 @@ class BulkOperationsMixin(object): dirty = self._end_outermost_bulk_operation(bulk_ops_record, structure_key) - self._clear_bulk_ops_record(structure_key) + # The bulk op has ended. However, the signal tasks below still need to use the + # built-up bulk op information (if the signals trigger tasks in the same thread). + # So re-nest until the signals are sent. + bulk_ops_record.nest() if emit_signals and dirty: self.send_bulk_published_signal(bulk_ops_record, structure_key) self.send_bulk_library_updated_signal(bulk_ops_record, structure_key) + # Signals are sent. Now unnest and clear the bulk op for good. + bulk_ops_record.unnest() + + self._clear_bulk_ops_record(structure_key) + def _is_in_bulk_operation(self, course_key, ignore_case=False): """ Return whether a bulk operation is active on `course_key`. diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index 08654425b3..e851f43c71 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -75,11 +75,11 @@ class SignalHandler(object): 1. We receive using the Django Signals mechanism. 2. The sender is going to be the class of the modulestore sending it. - 3. Always have **kwargs in your signal handler, as new things may be added. - 4. The thing that listens for the signal lives in process, but should do + 3. The names of your handler function's parameters *must* be "sender" and "course_key". + 4. Always have **kwargs in your signal handler, as new things may be added. + 5. The thing that listens for the signal lives in process, but should do almost no work. Its main job is to kick off the celery task that will do the actual work. - """ course_published = django.dispatch.Signal(providing_args=["course_key"]) library_updated = django.dispatch.Signal(providing_args=["library_key"]) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index 9a4ba7c6f1..d7ef5d542b 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -223,7 +223,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): self.course_id = course_key self.cached_metadata = cached_metadata - def load_item(self, location): + def load_item(self, location, for_parent=None): # pylint: disable=method-hidden """ Return an XModule instance for the specified location """ @@ -292,7 +292,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): field_data = KvsFieldData(kvs) scope_ids = ScopeIds(None, category, location, location) - module = self.construct_xblock_from_class(class_, scope_ids, field_data) + module = self.construct_xblock_from_class(class_, scope_ids, field_data, for_parent=for_parent) if self.cached_metadata is not None: # parent container pointers don't differentiate between draft and non-draft # so when we do the lookup, we should do so with a non-draft location @@ -883,7 +883,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo apply_cached_metadata=bool, using_descriptor_system="None|CachingDescriptorSystem" ) - def _load_item(self, course_key, item, data_cache, apply_cached_metadata=True, using_descriptor_system=None): + def _load_item(self, course_key, item, data_cache, + apply_cached_metadata=True, using_descriptor_system=None, for_parent=None): """ Load an XModuleDescriptor from item, using the children stored in data_cache @@ -898,6 +899,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo purposes. using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem to add data to, and to load the XBlocks from. + for_parent (:class:`XBlock`): The parent of the XBlock being loaded. """ course_key = self.fill_in_run(course_key) location = Location._from_deprecated_son(item['location'], course_key.run) @@ -942,9 +944,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo system.module_data.update(data_cache) system.cached_metadata.update(cached_metadata) - return system.load_item(location) + return system.load_item(location, for_parent=for_parent) - def _load_items(self, course_key, items, depth=0, using_descriptor_system=None): + def _load_items(self, course_key, items, depth=0, using_descriptor_system=None, for_parent=None): """ Load a list of xmodules from the data in items, with children cached up to specified depth @@ -960,7 +962,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo item, data_cache, using_descriptor_system=using_descriptor_system, - apply_cached_metadata=self._should_apply_cached_metadata(item, depth) + apply_cached_metadata=self._should_apply_cached_metadata(item, depth), + for_parent=for_parent, ) for item in items ] @@ -1078,7 +1081,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo except ItemNotFoundError: return False - def get_item(self, usage_key, depth=0, using_descriptor_system=None): + def get_item(self, usage_key, depth=0, using_descriptor_system=None, for_parent=None, **kwargs): """ Returns an XModuleDescriptor instance for the item at location. @@ -1101,7 +1104,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo usage_key.course_key, [item], depth, - using_descriptor_system=using_descriptor_system + using_descriptor_system=using_descriptor_system, + for_parent=for_parent, )[0] return module @@ -1293,6 +1297,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo # so we use the location for both. ScopeIds(None, block_type, location, location), dbmodel, + for_parent=kwargs.get('for_parent'), ) if fields is not None: for key, value in fields.iteritems(): @@ -1341,11 +1346,16 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo block_id: a unique identifier for the new item. If not supplied, a new identifier will be generated """ - xblock = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, **kwargs) # attach to parent if given - if 'detached' not in xblock._class_tags: - parent = self.get_item(parent_usage_key) + parent = None + if parent_usage_key is not None: + parent = self.get_item(parent_usage_key) + kwargs.setdefault('for_parent', parent) + + xblock = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, **kwargs) + + if parent is not None and 'detached' not in xblock._class_tags: # Originally added to support entrance exams (settings.FEATURES.get('ENTRANCE_EXAMS')) if kwargs.get('position') is None: parent.children.append(xblock.location) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py index 814058d145..8c181b2639 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py @@ -82,12 +82,14 @@ class DraftModuleStore(MongoModuleStore): """ def get_published(): return wrap_draft(super(DraftModuleStore, self).get_item( - usage_key, depth=depth, using_descriptor_system=using_descriptor_system + usage_key, depth=depth, using_descriptor_system=using_descriptor_system, + for_parent=kwargs.get('for_parent'), )) def get_draft(): return wrap_draft(super(DraftModuleStore, self).get_item( - as_draft(usage_key), depth=depth, using_descriptor_system=using_descriptor_system + as_draft(usage_key), depth=depth, using_descriptor_system=using_descriptor_system, + for_parent=kwargs.get('for_parent') )) # return the published version if ModuleStoreEnum.RevisionOption.published_only is requested diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py index dff8b0d853..f8f1dbe54e 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py @@ -227,6 +227,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): class_, ScopeIds(None, block_key.type, definition_id, block_locator), field_data, + for_parent=kwargs.get('for_parent') ) except Exception: # pylint: disable=broad-except log.warning("Failed to load descriptor", exc_info=True) diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py index fc179c81ba..abe9fd1543 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py @@ -21,7 +21,6 @@ from mongodb_proxy import autoretry_read, MongoProxy from xmodule.exceptions import HeartbeatFailure from xmodule.modulestore import BlockData from xmodule.modulestore.split_mongo import BlockKey -import dogstats_wrapper as dog_stats_api new_contract('BlockData', BlockData) @@ -42,9 +41,10 @@ class Tagger(object): An object used by :class:`QueryTimer` to allow timed code blocks to add measurements and tags to the timer. """ - def __init__(self): + def __init__(self, default_sample_rate): self.added_tags = [] self.measures = [] + self.sample_rate = default_sample_rate def measure(self, name, size): """ @@ -109,7 +109,7 @@ class QueryTimer(object): metric_name: The name used to aggregate all of these metrics. course_context: The course which the query is being made for. """ - tagger = Tagger() + tagger = Tagger(self._sample_rate) metric_name = "{}.{}".format(self._metric_base, metric_name) start = time() @@ -125,24 +125,24 @@ class QueryTimer(object): size, timestamp=end, tags=[tag for tag in tags if not tag.startswith('{}:'.format(metric_name))], - sample_rate=self._sample_rate, + sample_rate=tagger.sample_rate, ) dog_stats_api.histogram( '{}.duration'.format(metric_name), end - start, timestamp=end, tags=tags, - sample_rate=self._sample_rate, + sample_rate=tagger.sample_rate, ) dog_stats_api.increment( metric_name, timestamp=end, tags=tags, - sample_rate=self._sample_rate, + sample_rate=tagger.sample_rate, ) -TIMER = QueryTimer(__name__, 0.001) +TIMER = QueryTimer(__name__, 0.01) def structure_from_mongo(structure, course_context=None): @@ -222,33 +222,42 @@ class CourseStructureCache(object): except InvalidCacheBackendError: self.no_cache_found = True - def get(self, key): + def get(self, key, course_context=None): """Pull the compressed, pickled struct data from cache and deserialize.""" if self.no_cache_found: return None - compressed_pickled_data = self.cache.get(key) - if compressed_pickled_data is None: - return None - return pickle.loads(zlib.decompress(compressed_pickled_data)) + with TIMER.timer("CourseStructureCache.get", course_context) as tagger: + compressed_pickled_data = self.cache.get(key) + tagger.tag(from_cache=str(compressed_pickled_data is not None).lower()) - def set(self, key, structure): + if compressed_pickled_data is None: + # Always log cache misses, because they are unexpected + tagger.sample_rate = 1 + return None + + tagger.measure('compressed_size', len(compressed_pickled_data)) + + pickled_data = zlib.decompress(compressed_pickled_data) + tagger.measure('uncompressed_size', len(pickled_data)) + + return pickle.loads(pickled_data) + + def set(self, key, structure, course_context=None): """Given a structure, will pickle, compress, and write to cache.""" if self.no_cache_found: return None - pickled_data = pickle.dumps(structure, pickle.HIGHEST_PROTOCOL) - # 1 = Fastest (slightly larger results) - compressed_pickled_data = zlib.compress(pickled_data, 1) + with TIMER.timer("CourseStructureCache.set", course_context) as tagger: + pickled_data = pickle.dumps(structure, pickle.HIGHEST_PROTOCOL) + tagger.measure('uncompressed_size', len(pickled_data)) - # record compressed course structure sizes - dog_stats_api.histogram( - 'compressed_course_structure.size', - len(compressed_pickled_data), - tags=[key] - ) - # Stuctures are immutable, so we set a timeout of "never" - self.cache.set(key, compressed_pickled_data, None) + # 1 = Fastest (slightly larger results) + compressed_pickled_data = zlib.compress(pickled_data, 1) + tagger.measure('compressed_size', len(compressed_pickled_data)) + + # Stuctures are immutable, so we set a timeout of "never" + self.cache.set(key, compressed_pickled_data, None) class MongoConnection(object): @@ -311,14 +320,19 @@ class MongoConnection(object): with TIMER.timer("get_structure", course_context) as tagger_get_structure: cache = CourseStructureCache() - structure = cache.get(key) - tagger_get_structure.tag(from_cache='true' if structure else 'false') + structure = cache.get(key, course_context) + tagger_get_structure.tag(from_cache=str(bool(structure)).lower()) if not structure: + # Always log cache misses, because they are unexpected + tagger_get_structure.sample_rate = 1 + with TIMER.timer("get_structure.find_one", course_context) as tagger_find_one: doc = self.structures.find_one({'_id': key}) tagger_find_one.measure("blocks", len(doc['blocks'])) structure = structure_from_mongo(doc, course_context) - cache.set(key, structure) + tagger_find_one.sample_rate = 1 + + cache.set(key, structure, course_context) return structure diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 85d8e7f371..abfc1f9664 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -2,12 +2,15 @@ Factories for use in tests of XBlocks. """ +import functools import inspect import pprint -import threading -from uuid import uuid4 -from decorator import contextmanager import pymongo.message +import threading +import traceback +from collections import defaultdict +from decorator import contextmanager +from uuid import uuid4 from factory import Factory, Sequence, lazy_attribute_sequence, lazy_attribute from factory.containers import CyclicDefinitionError @@ -320,47 +323,160 @@ def check_number_of_calls(object_with_method, method_name, maximum_calls, minimu return check_sum_of_calls(object_with_method, [method_name], maximum_calls, minimum_calls) +class StackTraceCounter(object): + """ + A class that counts unique stack traces underneath a particular stack frame. + """ + def __init__(self, stack_depth, include_arguments=True): + """ + Arguments: + stack_depth (int): The number of stack frames above this constructor to capture. + include_arguments (bool): Whether to store the arguments that are passed + when capturing a stack trace. + """ + self.include_arguments = include_arguments + self._top_of_stack = traceback.extract_stack(limit=stack_depth)[0] + + if self.include_arguments: + self._stacks = defaultdict(lambda: defaultdict(int)) + else: + self._stacks = defaultdict(int) + + def capture_stack(self, args, kwargs): + """ + Record the stack frames starting at the caller of this method, and + ending at the top of the stack as defined by the ``stack_depth``. + + Arguments: + args: The positional arguments to capture at this stack frame + kwargs: The keyword arguments to capture at this stack frame + """ + # pylint: disable=broad-except + + stack = traceback.extract_stack()[:-2] + + if self._top_of_stack in stack: + stack = stack[stack.index(self._top_of_stack):] + + if self.include_arguments: + safe_args = [] + for arg in args: + try: + safe_args.append(repr(arg)) + except Exception as exc: + safe_args.append('=min_sends + """ + with check_sum_of_calls( + pymongo.message, + ['query', 'get_more'], + max_finds, + min_finds, + ): + if max_sends is not None or min_sends is not None: + with check_sum_of_calls( + pymongo.message, + # mongo < 2.6 uses insert, update, delete and _do_batched_insert. >= 2.6 _do_batched_write + ['insert', 'update', 'delete', '_do_batched_write_command', '_do_batched_insert', ], + max_sends if max_sends is not None else float("inf"), + min_sends if min_sends is not None else 0, + ): + yield + else: + yield + + @contextmanager def check_mongo_calls(num_finds=0, num_sends=None): """ @@ -391,24 +538,8 @@ def check_mongo_calls(num_finds=0, num_sends=None): :param num_sends: If none, don't instrument the send calls. If non-none, count and compare to the given int value. """ - with check_sum_of_calls( - pymongo.message, - ['query', 'get_more'], - num_finds, - num_finds - ): - if num_sends is not None: - with check_sum_of_calls( - pymongo.message, - # mongo < 2.6 uses insert, update, delete and _do_batched_insert. >= 2.6 _do_batched_write - ['insert', 'update', 'delete', '_do_batched_write_command', '_do_batched_insert', ], - num_sends, - num_sends - ): - yield - else: - yield - + with check_mongo_calls_range(num_finds, num_finds, num_sends, num_sends): + yield # This dict represents the attribute keys for a course's 'about' info. # Note: The 'video' attribute is intentionally excluded as it must be diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index c490b0f614..69f5a433a5 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -2084,8 +2084,11 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): course_key = course.id def _clear_bulk_ops_record(course_key): # pylint: disable=unused-argument - """ Check if the signal has been fired. """ - self.assertEqual(receiver.call_count, 0) + """ + Check if the signal has been fired. + The course_published signal fires before the _clear_bulk_ops_record. + """ + self.assertEqual(receiver.call_count, 1) with patch.object( self.store.thread_cache.default_store, '_clear_bulk_ops_record', wraps=_clear_bulk_ops_record diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 4996449c4f..8d8f28b0d8 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -60,7 +60,7 @@ DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': '' -class ReferenceTestXBlock(XBlock, XModuleMixin): +class ReferenceTestXBlock(XModuleMixin): """ Test xblock type to test the reference field types """ diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py b/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py index b2b9558cf7..c39bbf4534 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py @@ -101,7 +101,7 @@ def render_to_template_mock(*args): pass -class StubXBlock(XBlock, XModuleMixin, InheritanceMixin): +class StubXBlock(XModuleMixin, InheritanceMixin): """ Stub XBlock used for testing. """ diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 78ef1043d3..8950380615 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -250,9 +250,9 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): # TODO (vshnayder): we are somewhat architecturally confused in the loading code: # load_item should actually be get_instance, because it expects the course-specific # policy to be loaded. For now, just add the course_id here... - def load_item(usage_key): + def load_item(usage_key, for_parent=None): """Return the XBlock for the specified location""" - return xmlstore.get_item(usage_key) + return xmlstore.get_item(usage_key, for_parent=for_parent) resources_fs = OSFS(xmlstore.data_dir / course_dir) diff --git a/common/lib/xmodule/xmodule/tabs.py b/common/lib/xmodule/xmodule/tabs.py index 9a713ac046..8671d715cd 100644 --- a/common/lib/xmodule/xmodule/tabs.py +++ b/common/lib/xmodule/xmodule/tabs.py @@ -28,7 +28,8 @@ class CourseTab(object): # subclass, shared by all instances of the subclass. type = '' - # The title of the tab, which should be internationalized + # The title of the tab, which should be internationalized using + # ugettext_noop since the user won't be available in this context. title = None # Class property that specifies whether the tab can be hidden for a particular course diff --git a/common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml new file mode 100644 index 0000000000..b0882e6b3b --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml @@ -0,0 +1,69 @@ +--- +metadata: + display_name: Checkboxes with Hints and Feedback + markdown: | + + You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option. + + You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers. + + You can also add hints for learners. + + Be sure to select Settings to specify a Display Name and other values that apply. + + Use the following example problem as a model. + + >>Which of the following is a fruit? Check all that apply.<< + + [x] apple {{ selected: You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds. }, { unselected: Remember that an apple is also a fruit.}} + [x] pumpkin {{ selected: You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds. }, { unselected: Remember that a pumpkin is also a fruit.}} + [ ] potato {{ U: You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form.}, { S: A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds.}} + [x] tomato {{ S: You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds. }, { U: Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}} + + + {{ ((A B D)) An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. }} + {{ ((A B C D)) You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is a vegetable. }} + + ||A fruit is the fertilized ovary from a flower.|| + ||A fruit contains seeds of the plant.|| + +tab: hint +data: | + + +

      You can provide feedback for each option in a checkbox problem, with distinct feedback depending on whether or not the learner selects that option.

      + +

      You can also provide compound feedback for a specific combination of answers. For example, if you have three possible answers in the problem, you can configure specific feedback for when a learner selects each combination of possible answers.

      + +

      You can also add hints for learners.

      + +

      Use the following example problem as a model.

      + +

      Which of the following is a fruit? Check all that apply.

      + + + apple + You are correct that an apple is a fruit because it is the fertilized ovary that comes from an apple tree and contains seeds. + Remember that an apple is also a fruit. + + pumpkin + You are correct that a pumpkin is a fruit because it is the fertilized ovary of a squash plant and contains seeds. + Remember that a pumpkin is also a fruit. + + potato + A potato is a vegetable, not a fruit, because it does not come from a flower and does not contain seeds. + You are correct that a potato is a vegetable because it is an edible part of a plant in tuber form. + + tomato + You are correct that a tomato is a fruit because it is the fertilized ovary of a tomato plant and contains seeds. + Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it a fruit. + + An apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. + You are correct that an apple, pumpkin, and tomato are all fruits as they all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an edible part of a plant in tuber form and is classified as a vegetable. + + + + A fruit is the fertilized ovary from a flower. + A fruit contains seeds of the plant. + +
      diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml new file mode 100644 index 0000000000..acace4cc9f --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml @@ -0,0 +1,46 @@ +--- +metadata: + display_name: Multiple Choice with Hints and Feedback + markdown: | + + You can provide feedback for each option in a multiple choice problem. + + You can also add hints for learners. + + Be sure to select Settings to specify a Display Name and other values that apply. + + Use the following example problem as a model. + + >>Which of the following is a vegetable?<< + ( ) apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}} + ( ) pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}} + (x) potato {{A potato is an edible part of a plant in tuber form and is a vegetable.}} + ( ) tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}} + + ||A fruit is the fertilized ovary from a flower.|| + ||A fruit contains seeds of the plant.|| + +tab: hint +data: | + + +

      You can provide feedback for each option in a multiple choice problem.

      + +

      You can also add hints for learners.

      + +

      Use the following example problem as a model.

      + +

      Which of the following is a vegetable?

      + + + apple An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit. + pumpkin A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit. + potato A potato is an edible part of a plant in tuber form and is a vegetable. + tomato Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit. + + + + A fruit is the fertilized ovary from a flower. + A fruit contains seeds of the plant. + +
      diff --git a/common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml new file mode 100644 index 0000000000..0f00b7760b --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml @@ -0,0 +1,54 @@ +--- +metadata: + display_name: Numerical Input with Hints and Feedback + markdown: | + + You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers. + + Use feedback for the correct answer to reinforce the process for arriving at the numerical value. + + You can also add hints for learners. + + Be sure to select Settings to specify a Display Name and other values that apply. + + Use the following example problem as a model. + + >>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)<< + + = 4 {{The mean for this set of numbers is 20 / 5, which equals 4.}} + + ||The mean is calculated by summing the set of numbers and dividing by n.|| + ||n is the count of items in the set.|| + + [explanation] + The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4. + [explanation] + +tab: hint +data: | + + +

      You can provide feedback for correct answers in numerical input problems. You cannot provide feedback for incorrect answers.

      + +

      Use feedback for the correct answer to reinforce the process for arriving at the numerical value.

      + +

      Use the following example problem as a model.

      + +

      What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)

      + + + The mean for this set of numbers is 20 / 5, which equals 4. + + + +
      +

      Explanation

      +

      The mean is calculated by summing the set of numbers and dividing by n. In this case: (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.

      +
      +
      + + + The mean is calculated by summing the set of numbers and dividing by n. + n is the count of items in the set. + +
      \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml new file mode 100644 index 0000000000..c7674648f6 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml @@ -0,0 +1,51 @@ +--- +metadata: + display_name: Dropdown with Hints and Feedback + markdown: | + + You can provide feedback for each available option in a dropdown problem. + + You can also add hints for learners. + + Be sure to select Settings to specify a Display Name and other values that apply. + + Use the following example problem as a model. + + >> A/an ________ is a vegetable.<< + + [[ + apple {{An apple is the fertilized ovary that comes from an apple tree and contains seeds, meaning it is a fruit.}} + pumpkin {{A pumpkin is the fertilized ovary of a squash plant and contains seeds, meaning it is a fruit.}} + (potato) {{A potato is an edible part of a plant in tuber form and is a vegetable.}} + tomato {{Many people mistakenly think a tomato is a vegetable. However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.}} + ]] + + ||A fruit is the fertilized ovary from a flower.|| + ||A fruit contains seeds of the plant.|| + +tab: hint +data: | + + +

      You can provide feedback for each available option in a dropdown problem.

      + +

      You can also add hints for learners.

      + +

      Use the following example problem as a model.

      + +

      A/an ________ is a vegetable.

      +
      + + + + + + + + + + + A fruit is the fertilized ovary from a flower. + A fruit contains seeds of the plant. + +
      diff --git a/common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml b/common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml new file mode 100644 index 0000000000..bcd6bf6813 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml @@ -0,0 +1,54 @@ +--- +metadata: + display_name: Text Input with Hints and Feedback + markdown: | + + You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers. + + Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer. + + Be sure to select Settings to specify a Display Name and other values that apply. + + Use the following example problem as a model. + + >>Which U.S. state has the largest land area?<< + + =Alaska {{Alaska is 576,400 square miles, more than double the land area + of the second largest state, Texas.}} + + not=Texas {{While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles.}} + + not=California {{California is the third largest state, with 155,959 square miles.}} + + ||Consider the square miles, not population.|| + ||Consider all 50 states, not just the continental United States.|| + +tab: hint +data: | + + +

      You can provide feedback for the correct answer in text input problems, as well as for specific incorrect answers.

      + +

      Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on how to arrive at the correct answer.

      + +

      Use the following example problem as a model.

      + +

      Which U.S. state has the largest land area?

      + + + + Alaska is 576,400 square miles, more than double the land area of the second largest state, Texas. + + While many people think Texas is the largest state, it is actually the second largest, with 261,797 square miles. + + California is the third largest state, with 155,959 square miles. + + + + + + Consider the square miles, not population. + Consider all 50 states, not just the continental United States. + + +
      diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py index 4dc087fd5e..d392de1829 100644 --- a/common/lib/xmodule/xmodule/tests/test_conditional.py +++ b/common/lib/xmodule/xmodule/tests/test_conditional.py @@ -79,10 +79,14 @@ class ConditionalFactory(object): child_descriptor.render = lambda view, context=None: descriptor_system.render(child_descriptor, view, context) child_descriptor.location = source_location.replace(category='html', name='child') - descriptor_system.load_item = { - child_descriptor.location: child_descriptor, - source_location: source_descriptor - }.get + def load_item(usage_id, for_parent=None): # pylint: disable=unused-argument + """Test-only implementation of load_item that simply returns static xblocks.""" + return { + child_descriptor.location: child_descriptor, + source_location: source_descriptor + }.get(usage_id) + + descriptor_system.load_item = load_item system.descriptor_runtime = descriptor_system diff --git a/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py new file mode 100644 index 0000000000..aaafca9454 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py @@ -0,0 +1,225 @@ +""" +Tests for course_metadata_utils. +""" +from collections import namedtuple +from datetime import timedelta, datetime +from unittest import TestCase + +from django.utils.timezone import UTC +from django.utils.translation import ugettext + +from xmodule.course_metadata_utils import ( + clean_course_key, + url_name_for_course_location, + display_name_with_default, + number_for_course_location, + has_course_started, + has_course_ended, + DEFAULT_START_DATE, + course_start_date_is_default, + course_start_datetime_text, + course_end_datetime_text, + may_certify_for_course, +) +from xmodule.fields import Date +from xmodule.modulestore.tests.test_cross_modulestore_import_export import ( + MongoModulestoreBuilder, + VersioningModulestoreBuilder, + MixedModulestoreBuilder +) + + +_TODAY = datetime.now(UTC()) +_LAST_MONTH = _TODAY - timedelta(days=30) +_LAST_WEEK = _TODAY - timedelta(days=7) +_NEXT_WEEK = _TODAY + timedelta(days=7) + + +class CourseMetadataUtilsTestCase(TestCase): + """ + Tests for course_metadata_utils. + """ + + def setUp(self): + """ + Set up module store testing capabilities and initialize test courses. + """ + super(CourseMetadataUtilsTestCase, self).setUp() + + mongo_builder = MongoModulestoreBuilder() + split_builder = VersioningModulestoreBuilder() + mixed_builder = MixedModulestoreBuilder([('mongo', mongo_builder), ('split', split_builder)]) + + with mixed_builder.build_without_contentstore() as (__, mixed_store): + with mixed_store.default_store('mongo'): + self.demo_course = mixed_store.create_course( + org="edX", + course="DemoX.1", + run="Fall_2014", + user_id=-3, # -3 refers to a "testing user" + fields={ + "start": _LAST_MONTH, + "end": _LAST_WEEK + } + ) + with mixed_store.default_store('split'): + self.html_course = mixed_store.create_course( + org="UniversityX", + course="CS-203", + run="Y2096", + user_id=-3, # -3 refers to a "testing user" + fields={ + "start": _NEXT_WEEK, + "display_name": "Intro to " + } + ) + + def test_course_metadata_utils(self): + """ + Test every single function in course_metadata_utils. + """ + + def mock_strftime_localized(date_time, format_string): + """ + Mock version of strftime_localized used for testing purposes. + + Because we don't have a real implementation of strftime_localized + to work with (strftime_localized is provided by the XBlock runtime, + which we don't have access to for this test case), we must declare + this dummy implementation. This does NOT behave like a real + strftime_localized should. It purposely returns a really dumb value + that's only useful for testing purposes. + + Arguments: + date_time (datetime): datetime to be formatted. + format_string (str): format specifier. Valid values include: + - 'DATE_TIME' + - 'TIME' + - 'SHORT_DATE' + - 'LONG_DATE' + + Returns (str): format_string + " " + str(date_time) + """ + if format_string in ['DATE_TIME', 'TIME', 'SHORT_DATE', 'LONG_DATE']: + return format_string + " " + date_time.strftime("%Y-%m-%d %H:%M:%S") + else: + raise ValueError("Invalid format string :" + format_string) + + test_datetime = datetime(1945, 02, 06, 04, 20, 00, tzinfo=UTC()) + advertised_start_parsable = "2038-01-19 03:14:07" + advertised_start_bad_date = "215-01-01 10:10:10" + advertised_start_unparsable = "This coming fall" + + FunctionTest = namedtuple('FunctionTest', 'function scenarios') # pylint: disable=invalid-name + TestScenario = namedtuple('TestScenario', 'arguments expected_return') # pylint: disable=invalid-name + + function_tests = [ + FunctionTest(clean_course_key, [ + # Test with a Mongo course and '=' as padding. + TestScenario( + (self.demo_course.id, '='), + "course_MVSFQL2EMVWW6WBOGEXUMYLMNRPTEMBRGQ======" + ), + # Test with a Split course and '~' as padding. + TestScenario( + (self.html_course.id, '~'), + "course_MNXXK4TTMUWXMMJ2KVXGS5TFOJZWS5DZLAVUGUZNGIYDGK2ZGIYDSNQ~" + ), + ]), + FunctionTest(url_name_for_course_location, [ + TestScenario((self.demo_course.location,), self.demo_course.location.name), + TestScenario((self.html_course.location,), self.html_course.location.name), + ]), + FunctionTest(display_name_with_default, [ + # Test course with no display name. + TestScenario((self.demo_course,), "Empty"), + # Test course with a display name that contains characters that need escaping. + TestScenario((self.html_course,), "Intro to <html>"), + ]), + FunctionTest(number_for_course_location, [ + TestScenario((self.demo_course.location,), "DemoX.1"), + TestScenario((self.html_course.location,), "CS-203"), + ]), + FunctionTest(has_course_started, [ + TestScenario((self.demo_course.start,), True), + TestScenario((self.html_course.start,), False), + ]), + FunctionTest(has_course_ended, [ + TestScenario((self.demo_course.end,), True), + TestScenario((self.html_course.end,), False), + ]), + FunctionTest(course_start_date_is_default, [ + TestScenario((test_datetime, advertised_start_parsable), False), + TestScenario((test_datetime, None), False), + TestScenario((DEFAULT_START_DATE, advertised_start_parsable), False), + TestScenario((DEFAULT_START_DATE, None), True), + ]), + FunctionTest(course_start_datetime_text, [ + # Test parsable advertised start date. + # Expect start datetime to be parsed and formatted back into a string. + TestScenario( + (DEFAULT_START_DATE, advertised_start_parsable, 'DATE_TIME', ugettext, mock_strftime_localized), + mock_strftime_localized(Date().from_json(advertised_start_parsable), 'DATE_TIME') + " UTC" + ), + # Test un-parsable advertised start date. + # Expect date parsing to throw a ValueError, and the advertised + # start to be returned in Title Case. + TestScenario( + (test_datetime, advertised_start_unparsable, 'DATE_TIME', ugettext, mock_strftime_localized), + advertised_start_unparsable.title() + ), + # Test parsable advertised start date from before January 1, 1900. + # Expect mock_strftime_localized to throw a ValueError, and the + # advertised start to be returned in Title Case. + TestScenario( + (test_datetime, advertised_start_bad_date, 'DATE_TIME', ugettext, mock_strftime_localized), + advertised_start_bad_date.title() + ), + # Test without advertised start date, but with a set start datetime. + # Expect formatted datetime to be returned. + TestScenario( + (test_datetime, None, 'SHORT_DATE', ugettext, mock_strftime_localized), + mock_strftime_localized(test_datetime, 'SHORT_DATE') + ), + # Test without advertised start date and with default start datetime. + # Expect TBD to be returned. + TestScenario( + (DEFAULT_START_DATE, None, 'SHORT_DATE', ugettext, mock_strftime_localized), + # Translators: TBD stands for 'To Be Determined' and is used when a course + # does not yet have an announced start date. + ugettext('TBD') + ) + ]), + FunctionTest(course_end_datetime_text, [ + # Test with a set end datetime. + # Expect formatted datetime to be returned. + TestScenario( + (test_datetime, 'TIME', mock_strftime_localized), + mock_strftime_localized(test_datetime, 'TIME') + " UTC" + ), + # Test with default end datetime. + # Expect empty string to be returned. + TestScenario( + (None, 'TIME', mock_strftime_localized), + "" + ) + ]), + FunctionTest(may_certify_for_course, [ + TestScenario(('early_with_info', True, True), True), + TestScenario(('early_no_info', False, False), True), + TestScenario(('end', True, False), True), + TestScenario(('end', False, True), True), + TestScenario(('end', False, False), False), + ]), + ] + + for function_test in function_tests: + for scenario in function_test.scenarios: + actual_return = function_test.function(*scenario.arguments) + self.assertEqual(actual_return, scenario.expected_return) + + # Even though we don't care about testing mock_strftime_localized, + # we still need to test it with a bad format string in order to + # satisfy the coverage checker. + with self.assertRaises(ValueError): + mock_strftime_localized(test_datetime, 'BAD_FORMAT_SPECIFIER') diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index a9008d5511..da05da360e 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -19,6 +19,10 @@ COURSE = 'test_course' NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC()) +_TODAY = datetime.now(UTC()) +_LAST_WEEK = _TODAY - timedelta(days=7) +_NEXT_WEEK = _TODAY + timedelta(days=7) + class CourseFieldsTestCase(unittest.TestCase): def test_default_start_date(self): @@ -348,3 +352,49 @@ class TeamsConfigurationTestCase(unittest.TestCase): self.add_team_configuration(max_team_size=4, topics=topics) self.assertTrue(self.course.teams_enabled) self.assertEqual(self.course.teams_topics, topics) + + +class CourseDescriptorTestCase(unittest.TestCase): + """ + Tests for a select few functions from CourseDescriptor. + + I wrote these test functions in order to satisfy the coverage checker for + PR #8484, which modified some code within CourseDescriptor. However, this + class definitely isn't a comprehensive test case for CourseDescriptor, as + writing a such a test case was out of the scope of the PR. + """ + + def setUp(self): + """ + Initialize dummy testing course. + """ + super(CourseDescriptorTestCase, self).setUp() + self.course = get_dummy_course(start=_TODAY) + + def test_clean_id(self): + """ + Test CourseDescriptor.clean_id. + """ + self.assertEqual( + self.course.clean_id(), + "course_ORSXG5C7N5ZGOL3UMVZXIX3DN52XE43FF52GK43UL5ZHK3Q=" + ) + self.assertEqual( + self.course.clean_id(padding_char='$'), + "course_ORSXG5C7N5ZGOL3UMVZXIX3DN52XE43FF52GK43UL5ZHK3Q$" + ) + + def test_has_started(self): + """ + Test CourseDescriptor.has_started. + """ + self.course.start = _LAST_WEEK + self.assertTrue(self.course.has_started()) + self.course.start = _NEXT_WEEK + self.assertFalse(self.course.has_started()) + + def test_number(self): + """ + Test CourseDescriptor.number. + """ + self.assertEqual(self.course.number, COURSE) diff --git a/common/lib/xmodule/xmodule/tests/test_error_module.py b/common/lib/xmodule/xmodule/tests/test_error_module.py index 1b12017fbd..a59cc56452 100644 --- a/common/lib/xmodule/xmodule/tests/test_error_module.py +++ b/common/lib/xmodule/xmodule/tests/test_error_module.py @@ -9,7 +9,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW from mock import MagicMock, Mock, patch from xblock.runtime import Runtime, IdReader -from xblock.field_data import FieldData +from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from xblock.test.tools import unabc @@ -43,10 +43,11 @@ class TestErrorModule(SetupTestErrorModules): self.assertIn(repr(self.valid_xml), context_repr) def test_error_module_from_descriptor(self): - descriptor = MagicMock([XModuleDescriptor], - runtime=self.system, - location=self.location, - _field_data=self.valid_xml) + descriptor = MagicMock( + spec=XModuleDescriptor, + runtime=self.system, + location=self.location, + ) error_descriptor = ErrorDescriptor.from_descriptor( descriptor, self.error_msg) @@ -81,10 +82,11 @@ class TestNonStaffErrorModule(SetupTestErrorModules): self.assertNotIn(repr(self.valid_xml), context_repr) def test_error_module_from_descriptor(self): - descriptor = MagicMock([XModuleDescriptor], - runtime=self.system, - location=self.location, - _field_data=self.valid_xml) + descriptor = MagicMock( + spec=XModuleDescriptor, + runtime=self.system, + location=self.location, + ) error_descriptor = NonStaffErrorDescriptor.from_descriptor( descriptor, self.error_msg) @@ -122,7 +124,7 @@ class TestErrorModuleConstruction(unittest.TestCase): def setUp(self): # pylint: disable=abstract-class-instantiated super(TestErrorModuleConstruction, self).setUp() - field_data = Mock(spec=FieldData) + field_data = DictFieldData({}) self.descriptor = BrokenDescriptor( TestRuntime(Mock(spec=IdReader), field_data), field_data, diff --git a/common/lib/xmodule/xmodule/tests/xml/__init__.py b/common/lib/xmodule/xmodule/tests/xml/__init__.py index c6ec34785c..f4342b45b6 100644 --- a/common/lib/xmodule/xmodule/tests/xml/__init__.py +++ b/common/lib/xmodule/xmodule/tests/xml/__init__.py @@ -49,7 +49,7 @@ class InMemorySystem(XMLParsingSystem, MakoDescriptorSystem): # pylint: disable self._descriptors[descriptor.location.to_deprecated_string()] = descriptor return descriptor - def load_item(self, location): # pylint: disable=method-hidden + def load_item(self, location, for_parent=None): # pylint: disable=method-hidden, unused-argument """Return the descriptor loaded for `location`""" return self._descriptors[location.to_deprecated_string()] diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index fc76bb6c0c..f9134c820c 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -86,7 +86,7 @@ log = logging.getLogger(__name__) _ = lambda text: text -class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule): +class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule, LicenseMixin): """ XML source example:
      diff --git a/common/test/acceptance/pages/lms/discovery.py b/common/test/acceptance/pages/lms/discovery.py index 00547ccf9d..b8aea204cd 100644 --- a/common/test/acceptance/pages/lms/discovery.py +++ b/common/test/acceptance/pages/lms/discovery.py @@ -15,7 +15,15 @@ class CourseDiscoveryPage(PageObject): form = "#discovery-form" def is_browser_on_page(self): - return "Courses" in self.browser.title + """ + Loading indicator must be present, but not visible + """ + loading_css = "#loading-indicator" + courses_css = '.courses-listing' + + return self.q(css=courses_css).visible \ + and self.q(css=loading_css).present \ + and not self.q(css=loading_css).visible @property def result_items(self): diff --git a/common/test/acceptance/pages/lms/fields.py b/common/test/acceptance/pages/lms/fields.py index 803f5a7886..014aa11bdb 100644 --- a/common/test/acceptance/pages/lms/fields.py +++ b/common/test/acceptance/pages/lms/fields.py @@ -211,6 +211,15 @@ class FieldsMixin(object): query = self.q(css='.u-field-link-title-{}'.format(field_id)) return query.text[0] if query.present else None + def wait_for_link_title_for_link_field(self, field_id, expected_title): + """ + Wait until the title of the specified link field equals expected_title. + """ + return EmptyPromise( + lambda: self.link_title_for_link_field(field_id) == expected_title, + "Link field with link title \"{0}\" is visible.".format(expected_title) + ).fulfill() + def click_on_link_in_link_field(self, field_id): """ Click the link in a link field. diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py index b61e25c547..9cbd6af99f 100644 --- a/common/test/acceptance/pages/lms/login_and_register.py +++ b/common/test/acceptance/pages/lms/login_and_register.py @@ -232,7 +232,7 @@ class CombinedLoginAndRegisterPage(PageObject): Only the "Dummy" provider is used for bok choy because it is the only one that doesn't send traffic to external servers. """ - self.q(css="button.{}-Dummy".format(self.current_form)).click() + self.q(css="button.{}-oa2-dummy".format(self.current_form)).click() def password_reset(self, email): """Navigates to, fills in, and submits the password reset form. @@ -281,6 +281,8 @@ class CombinedLoginAndRegisterPage(PageObject): return "login" elif self.q(css=".js-reset").visible: return "password-reset" + elif self.q(css=".proceed-button").visible: + return "hinted-login" @property def email_value(self): @@ -335,3 +337,9 @@ class CombinedLoginAndRegisterPage(PageObject): return (True, msg_element.text[0]) return (False, None) return Promise(_check_func, "Result of third party auth is visible").fulfill() + + @property + def hinted_login_prompt(self): + """Get the message displayed to the user on the hinted-login form""" + if self.q(css=".wrapper-other-login .instructions").visible: + return self.q(css=".wrapper-other-login .instructions").text[0] diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py index fa49ea824e..0183d5c3cd 100644 --- a/common/test/acceptance/pages/lms/problem.py +++ b/common/test/acceptance/pages/lms/problem.py @@ -66,7 +66,7 @@ class ProblemPage(PageObject): """ Is there a "correct" status showing? """ - return self.q(css="div.problem div.capa_inputtype.textline div.correct p.status").is_present() + return self.q(css="div.problem div.capa_inputtype.textline div.correct span.status").is_present() def click_clarification(self, index=0): """ diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index f031f7ee1b..79fe5419e7 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -18,4 +18,9 @@ class TeamsPage(CoursePage): def get_body_text(self): """ Returns the current dummy text. This will be changed once there is more content on the page. """ - return self.q(css='.page-content-main').text[0] + main_page_content_css = '.page-content-main' + self.wait_for( + lambda: len(self.q(css=main_page_content_css).text) == 1, + description="Body text is present" + ) + return self.q(css=main_page_content_css).text[0] diff --git a/common/test/acceptance/pages/studio/import_export.py b/common/test/acceptance/pages/studio/import_export.py index 15859af15e..1d8587ebf0 100644 --- a/common/test/acceptance/pages/studio/import_export.py +++ b/common/test/acceptance/pages/studio/import_export.py @@ -239,10 +239,16 @@ class ImportMixin(object): def is_timestamp_visible(self): """ - Checks if the UTC timestamp of the last successfull import is visible + Checks if the UTC timestamp of the last successful import is visible """ return self.q(css='.item-progresspoint-success-date').visible + def wait_for_timestamp_visible(self): + """ + Wait for the timestamp of the last successful import to be visible. + """ + EmptyPromise(self.is_timestamp_visible, 'Timestamp Visible', timeout=30).fulfill() + def wait_for_filename_error(self): """ Wait for the upload field to display an error. diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py index c9ab3eb051..efdcd1c00b 100644 --- a/common/test/acceptance/tests/lms/test_account_settings.py +++ b/common/test/acceptance/tests/lms/test_account_settings.py @@ -437,9 +437,10 @@ class AccountSettingsPageTest(AccountSettingsTestMixin, WebAppTest): Currently there is no way to test the whole authentication process because that would require accounts with the providers. """ - for field_id, title, link_title in [ - ['auth-facebook', 'Facebook', 'Link'], - ['auth-google', 'Google', 'Link'], - ]: + providers = ( + ['auth-oa2-facebook', 'Facebook', 'Link'], + ['auth-oa2-google-oauth2', 'Google', 'Link'], + ) + for field_id, title, link_title in providers: self.assertEqual(self.account_settings_page.title_for_field(field_id), title) self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title) diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index ad885e34dc..00eb2b33cc 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -164,9 +164,43 @@ class LoginFromCombinedPageTest(UniqueCourseTest): self.dashboard_page.wait_for_page() - # Now unlink the account (To test the account settings view and also to prevent cross-test side effects) + self._unlink_dummy_account() + + def test_hinted_login(self): + """ Test the login page when coming from course URL that specified which third party provider to use """ + # Create a user account and link it to third party auth with the dummy provider: + AutoAuthPage(self.browser, course_id=self.course_id).visit() + self._link_dummy_account() + LogoutPage(self.browser).visit() + + # When not logged in, try to load a course URL that includes the provider hint ?tpa_hint=... + course_page = CoursewarePage(self.browser, self.course_id) + self.browser.get(course_page.url + '?tpa_hint=oa2-dummy') + + # We should now be redirected to the login page + self.login_page.wait_for_page() + self.assertIn("Would you like to sign in using your Dummy credentials?", self.login_page.hinted_login_prompt) + self.login_page.click_third_party_dummy_provider() + + # We should now be redirected to the course page + course_page.wait_for_page() + + self._unlink_dummy_account() + + def _link_dummy_account(self): + """ Go to Account Settings page and link the user's account to the Dummy provider """ account_settings = AccountSettingsPage(self.browser).visit() - field_id = "auth-dummy" + field_id = "auth-oa2-dummy" + account_settings.wait_for_field(field_id) + self.assertEqual("Link", account_settings.link_title_for_link_field(field_id)) + account_settings.click_on_link_in_link_field(field_id) + account_settings.wait_for_link_title_for_link_field(field_id, "Unlink") + + def _unlink_dummy_account(self): + """ Verify that the 'Dummy' third party auth provider is linked, then unlink it """ + # This must be done after linking the account, or we'll get cross-test side effects + account_settings = AccountSettingsPage(self.browser).visit() + field_id = "auth-oa2-dummy" account_settings.wait_for_field(field_id) self.assertEqual("Unlink", account_settings.link_title_for_link_field(field_id)) account_settings.click_on_link_in_link_field(field_id) @@ -305,7 +339,7 @@ class RegisterFromCombinedPageTest(UniqueCourseTest): # Now unlink the account (To test the account settings view and also to prevent cross-test side effects) account_settings = AccountSettingsPage(self.browser).visit() - field_id = "auth-dummy" + field_id = "auth-oa2-dummy" account_settings.wait_for_field(field_id) self.assertEqual("Unlink", account_settings.link_title_for_link_field(field_id)) account_settings.click_on_link_in_link_field(field_id) diff --git a/common/test/acceptance/tests/lms/test_lms_course_discovery.py b/common/test/acceptance/tests/lms/test_lms_course_discovery.py index 7f9231dcbd..084a15b4e5 100644 --- a/common/test/acceptance/tests/lms/test_lms_course_discovery.py +++ b/common/test/acceptance/tests/lms/test_lms_course_discovery.py @@ -72,7 +72,6 @@ class CourseDiscoveryTest(WebAppTest): """ self.page.visit() - @flaky # TODO: fix this. See SOL-975 def test_search(self): """ Make sure you can search for courses. diff --git a/common/test/acceptance/tests/studio/test_import_export.py b/common/test/acceptance/tests/studio/test_import_export.py index 67af0cd00e..beadbafb68 100644 --- a/common/test/acceptance/tests/studio/test_import_export.py +++ b/common/test/acceptance/tests/studio/test_import_export.py @@ -4,7 +4,6 @@ Acceptance tests for the Import and Export pages from abc import abstractmethod from bok_choy.promise import EmptyPromise from datetime import datetime -from flaky import flaky from .base_studio_test import StudioLibraryTest, StudioCourseTest from ...fixtures.course import XBlockFixtureDesc @@ -186,7 +185,6 @@ class ImportTestMixin(object): self.import_page.upload_tarball(self.tarball_name) self.import_page.wait_for_upload() - @flaky # TODO: fix this. See TNL-2386 def test_import_timestamp(self): """ Scenario: I perform a course / library import @@ -200,13 +198,13 @@ class ImportTestMixin(object): utc_now = datetime.utcnow() import_date, import_time = self.import_page.timestamp - self.assertTrue(self.import_page.is_timestamp_visible()) + self.import_page.wait_for_timestamp_visible() self.assertEqual(utc_now.strftime('%m/%d/%Y'), import_date) self.assertEqual(utc_now.strftime('%H:%M'), import_time) self.import_page.visit() self.import_page.wait_for_tasks(completed=True) - self.assertTrue(self.import_page.is_timestamp_visible()) + self.import_page.wait_for_timestamp_visible() def test_landing_url(self): """ diff --git a/common/test/acceptance/tests/studio/test_studio_settings_details.py b/common/test/acceptance/tests/studio/test_studio_settings_details.py index f81e1cae82..76538422f0 100644 --- a/common/test/acceptance/tests/studio/test_studio_settings_details.py +++ b/common/test/acceptance/tests/studio/test_studio_settings_details.py @@ -1,6 +1,7 @@ """ Acceptance tests for Studio's Settings Details pages """ +from flaky import flaky from unittest import skip from .base_studio_test import StudioCourseTest @@ -40,6 +41,7 @@ class SettingsMilestonesTest(StudioCourseTest): self.assertTrue(self.settings_detail.pre_requisite_course_options) + @flaky # TODO: fix this. SOL-449 def test_prerequisite_course_save_successfully(self): """ Scenario: Selecting course from Pre-Requisite course drop down save the selected course as pre-requisite diff --git a/common/test/db_fixtures/third_party_auth.json b/common/test/db_fixtures/third_party_auth.json new file mode 100644 index 0000000000..3042ebbb66 --- /dev/null +++ b/common/test/db_fixtures/third_party_auth.json @@ -0,0 +1,47 @@ +[ + { + "pk": 1, + "model": "third_party_auth.oauth2providerconfig", + "fields": { + "enabled": true, + "change_date": "2001-02-03T04:05:06Z", + "changed_by": null, + "name": "Google", + "icon_class": "fa-google-plus", + "backend_name": "google-oauth2", + "key": "test", + "secret": "test", + "other_settings": "{}" + } + }, + { + "pk": 2, + "model": "third_party_auth.oauth2providerconfig", + "fields": { + "enabled": true, + "change_date": "2001-02-03T04:05:06Z", + "changed_by": null, + "name": "Facebook", + "icon_class": "fa-facebook", + "backend_name": "facebook", + "key": "test", + "secret": "test", + "other_settings": "{}" + } + }, + { + "pk": 3, + "model": "third_party_auth.oauth2providerconfig", + "fields": { + "enabled": true, + "change_date": "2001-02-03T04:05:06Z", + "changed_by": null, + "name": "Dummy", + "icon_class": "fa-sign-in", + "backend_name": "dummy", + "key": "", + "secret": "", + "other_settings": "{}" + } + } +] diff --git a/conf/locale/ar/LC_MESSAGES/django.mo b/conf/locale/ar/LC_MESSAGES/django.mo index 39079e1d91..049128189c 100644 Binary files a/conf/locale/ar/LC_MESSAGES/django.mo and b/conf/locale/ar/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ar/LC_MESSAGES/django.po b/conf/locale/ar/LC_MESSAGES/django.po index fa6759431f..2ebb1f8600 100644 --- a/conf/locale/ar/LC_MESSAGES/django.po +++ b/conf/locale/ar/LC_MESSAGES/django.po @@ -125,7 +125,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" "PO-Revision-Date: 2015-05-28 20:00+0000\n" "Last-Translator: Nabeel El-Dughailib \n" "Language-Team: Arabic (http://www.transifex.com/projects/p/edx-platform/language/ar/)\n" @@ -205,13 +205,8 @@ msgstr "" #: lms/djangoapps/shoppingcart/reports.py #: lms/templates/open_ended_problems/open_ended_problems.html #: lms/templates/shoppingcart/receipt.html -#, fuzzy msgid "Status" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"الحالة\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"حالة" +msgstr "الحالة" #: common/djangoapps/config_models/admin.py msgid "Revert to the selected configuration" @@ -292,13 +287,8 @@ msgstr "" #: common/djangoapps/course_modes/models.py lms/djangoapps/branding/api.py #: openedx/core/djangoapps/user_api/views.py #: lms/templates/static_templates/honor.html -#, fuzzy msgid "Honor Code" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"ميثاق الشرف \n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"ميثاق الشرف" +msgstr "ميثاق الشرف" #: common/djangoapps/course_modes/models.py msgid "You're auditing this course" @@ -537,13 +527,8 @@ msgstr "المدرسة الابتدائية" #. Translators: 'None' refers to the student's level of education #: common/djangoapps/student/models.py #: common/lib/xmodule/xmodule/course_module.py cms/templates/settings.html -#, fuzzy msgid "None" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"لا شيء\n" -"#-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-#\n" -"غير مذكور" +msgstr "لا شيء" #: common/djangoapps/student/models.py msgid "{platform_name} Honor Code Certificate for {course_name}" @@ -670,13 +655,8 @@ msgid "Too many failed login attempts. Try again later." msgstr "محاولات فاشلة كثيرة لتسجيل الدخول. يُرجى إعادة المحاولة لاحقًا. " #: common/djangoapps/student/views.py lms/templates/provider_login.html -#, fuzzy msgid "Email or password is incorrect." -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"يوجد خطأ إمّا في عنوان البريد الإلكتروني أو في كلمة السر.\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"البريد الإلكتروني أو كلمة السر غير صحيحين" +msgstr "يوجد خطأ إمّا في عنوان البريد الإلكتروني أو في كلمة السر." #: common/djangoapps/student/views.py msgid "" @@ -823,13 +803,28 @@ msgstr "" "نأسف لتعذّر إمكانية إرسال رابط تفعيل البريد الإلكتروني. يُرجى إعادة المحاولة" " لاحقًا. " -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "يُرجى إدخال الاسم" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "الرقم التعريفي غير صحيح" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1281,6 +1276,24 @@ msgstr "لا توجد إجابة " msgid "processing" msgstr "جاري العمل" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -1433,13 +1446,8 @@ msgstr "القائمة المنسدلة " #: common/lib/capa/capa/responsetypes.py #: cms/templates/widgets/problem-edit.html -#, fuzzy msgid "Numerical Input" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"مُدخَل رقمي\n" -"#-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-#\n" -"مدخلات رقمية" +msgstr "مُدخَل رقمي" #: common/lib/capa/capa/responsetypes.py msgid "There was a problem with the staff answer to this problem." @@ -1488,13 +1496,8 @@ msgstr "" #: common/lib/capa/capa/responsetypes.py #: cms/templates/widgets/problem-edit.html -#, fuzzy msgid "Text Input" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"مُدخَل نصّي\n" -"#-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-#\n" -"مدخلات النصّ" +msgstr "مُدخَل نصّي" #: common/lib/capa/capa/responsetypes.py msgid "error" @@ -1661,13 +1664,8 @@ msgstr "بيانات XML للملاحظات التوضيحية" #: common/lib/xmodule/xmodule/videoannotation_module.py #: common/lib/xmodule/xmodule/word_cloud_module.py #: cms/templates/container.html cms/templates/library.html -#, fuzzy msgid "Display Name" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"اسم العرض\n" -"#-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-#\n" -"إظهار الاسم " +msgstr "اسم العرض" #: common/lib/xmodule/xmodule/annotatable_module.py #: common/lib/xmodule/xmodule/discussion_module.py @@ -2057,13 +2055,8 @@ msgstr " الخطوة التي يعمل عليها الطالب ضمن المه #: common/lib/xmodule/xmodule/combined_open_ended_module.py #: common/lib/xmodule/xmodule/peer_grading_module.py #: lms/templates/peer_grading/peer_grading.html -#, fuzzy msgid "Graded" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"جرى التقييم\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"تم تقييمه" +msgstr "جرى التقييم" #: common/lib/xmodule/xmodule/combined_open_ended_module.py msgid "" @@ -2181,6 +2174,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "سيُحدَّد لاحقًا " + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "تصاريح قابلية التشغيل البيني لأدوات التعلّم LTI" @@ -3120,13 +3123,6 @@ msgstr "" msgid "General" msgstr "عام" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "سيُحدَّد لاحقًا " - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "رقم المناقشة" @@ -5525,13 +5521,8 @@ msgstr "الأخطاء في المساق {course_name} الذي جرى تحمي #: lms/djangoapps/dashboard/sysadmin.py cms/templates/course-create-rerun.html #: cms/templates/index.html lms/templates/shoppingcart/receipt.html -#, fuzzy msgid "Course Name" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"اسم المساق \n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"اسم المساق" +msgstr "اسم المساق" #: lms/djangoapps/dashboard/sysadmin.py msgid "Directory/ID" @@ -5804,13 +5795,8 @@ msgstr "رقم المستخدم" #: openedx/core/djangoapps/user_api/views.py #: lms/templates/courseware/legacy_instructor_dashboard.html #: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html -#, fuzzy msgid "Email" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"عنوان البريد الإلكتروني\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"البريد الإلكتروني" +msgstr "عنوان البريد الإلكتروني" #: lms/djangoapps/instructor/views/api.py #: lms/djangoapps/instructor_task/tasks_helper.py lms/envs/devstack.py @@ -6216,13 +6202,8 @@ msgstr "البيانات غير مكتملة" #: lms/djangoapps/instructor/views/legacy.py #: lms/templates/courseware/legacy_instructor_dashboard.html -#, fuzzy msgid "Course Statistics At A Glance" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"لمحة سريعة عن إحصاءات المساق\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"لمحة سريعة عن إحصائيات المساق" +msgstr "لمحة سريعة عن إحصاءات المساق" #: lms/djangoapps/instructor/views/legacy.py msgid "List of students enrolled in {course_key}" @@ -6314,15 +6295,9 @@ msgstr "الرقم التعريفي" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html -#, fuzzy msgid "Full Name" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"الاسم الكامل\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"الإسم الكامل" +msgstr "الاسم الكامل" #: lms/djangoapps/instructor/views/legacy.py msgid "edX email" @@ -6407,13 +6382,8 @@ msgstr "إعادة الضبط" #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py #: lms/templates/wiki/plugins/attachments/index.html wiki/models/article.py -#, fuzzy msgid "deleted" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"الحذف\n" -"#-#-#-#-# wiki.po (edx-platform) #-#-#-#-#\n" -"محذوف" +msgstr "الحذف" #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. @@ -7210,7 +7180,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "يجب أن تكون مسجَّل الدخول لتتمكّن من الإضافة إلى سلّة التسوّق" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "المساق الذي طلبته غير موجود." @@ -7922,27 +7891,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -#, fuzzy -msgid "Download CSV Reports" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"تنزيل تقارير CSV \n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"تنزيل الإبلاغات بصيغة CSV " - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -#, fuzzy -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"نأسف لحدوث خطأ في عملية إدخال التاريخ التي قمت بها. يجب صياغته بهذا الشكل YYYY-MM-DD\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"حدث خطأ في عملية إدخال التاريخ التي قمت بها. يجب أن يتم إدخاله على الشكل YYYY-MM-DD" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "لا يوجد مستخدم يمتلك عنوان البريد الإلكتروني المحدَّد." @@ -8046,14 +7994,8 @@ msgid "Payment confirmation" msgstr "تأكيد الدفعة" #: lms/djangoapps/verify_student/views.py -#: lms/templates/verify_student/photo_reverification.html -#, fuzzy msgid "Take photo" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"التقاط صورة \n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"قم بأخذ صورة " +msgstr "التقاط صورة " #: lms/djangoapps/verify_student/views.py msgid "Take a photo of your ID" @@ -8784,13 +8726,8 @@ msgstr "" #: lms/templates/wiki/plugins/attachments/index.html #: wiki/plugins/attachments/wiki_plugin.py -#, fuzzy msgid "Attachments" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"المرفقات\n" -"#-#-#-#-# wiki.po (edx-platform) #-#-#-#-#\n" -"الملفّات المرفقة" +msgstr "المرفقات" #: lms/templates/wiki/plugins/attachments/index.html msgid "Upload new file" @@ -9276,6 +9213,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "رقم المساق الأساسي غير صالح" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "إنّ إعدادات هذه المجموعة قيد الاستخدام ولا يمكن حذفها." @@ -9471,13 +9416,8 @@ msgstr "تاريخ الإضافة " #. duration is determined. #: cms/djangoapps/contentstore/views/videos.py cms/templates/index.html #: lms/templates/dashboard/_dashboard_status_verification.html -#, fuzzy msgid "Pending" -msgstr "" -"#-#-#-#-# django-studio.po (edx-platform) #-#-#-#-#\n" -"مُعلَّق\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"معلَّق" +msgstr "مُعلَّق" #. Translators: This is the suggested filename when downloading the URL #. listing for videos uploaded through Studio @@ -9545,7 +9485,6 @@ msgid "Loading" msgstr "جاري التحميل" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "إغلاق " @@ -9687,11 +9626,6 @@ msgstr "المساعدة" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -9720,10 +9654,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "يُرجى تحديث تسجيلك في {} | واختر مسارك" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "سجّل في {} | اختر مسارك" @@ -9732,6 +9662,10 @@ msgstr "سجّل في {} | اختر مسارك" msgid "Sorry, there was an error when trying to enroll you" msgstr "نأسف لحدوث خطأ ما عند محاولة تسجيلك" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -12593,16 +12527,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -12618,9 +12556,8 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" -msgstr "المزيد" +msgid "Less" +msgstr "" #: lms/templates/courseware/progress.html msgid "{earned:.3n} of {total:.3n} possible points" @@ -12920,6 +12857,37 @@ msgstr "" "الدفعة، أو يمكنك {unenroll_link_start}إلغاء التسجيل{unenroll_link_end} في " "هذا المساق." +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "تعديل" @@ -13405,6 +13373,10 @@ msgstr "إلغاء التثبيت" msgid "Open" msgstr "فتح" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "المزيد" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -16158,10 +16130,21 @@ msgstr "عرض المساقات" msgid "Payment" msgstr "الدفعة" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "تنزيل الإبلاغات بصيغة CSV " + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "تنزيل البيانات بصيغة CSV" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" +"حدث خطأ في عملية إدخال التاريخ التي قمت بها. يجب أن يتم إدخاله على الشكل " +"YYYY-MM-DD" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "تم الفصل ضمن هذه الإبلاغات باستخدام تواريخ البداية والنهاية." @@ -16497,14 +16480,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "{platform_name} - عربة التسوق" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "مراجعة " #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "تأكيد " @@ -16690,113 +16669,14 @@ msgstr "" "إذا كان لديك أي أسئلة حول المساق أو هذه الاستمارة، يُرجى الاتصال بـ {mail_to_link}." -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "تعديل اسمك " - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "حدث الخاطئ التالي أثناء تعديل إسمك:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"حرصاً على مصداقية شهادات {platform}، سيتم تدوين وتسجيل جميع التغييرات في " -"الأسماء." - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "أسباب تغيير الاسم:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "تغيير اسمي:" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "هل تواجه مشكلة تقنية؟ " - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" -"يرجى التأكد من تحديث متصفحك إلى {a_start}أحدث نسخة " -"متاحة{a_end}، كما يرجى التأكد من أن كاميرا الكومبيوتر لديك " -"متصلة ومشغّلة ومسموح بتشغيلها على متصفح الإنترنت لديك (عادة ما يمكن تنسيق " -"هذه الإعدادات من خلال إعدادات المتصفح لديك) " - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "هل لديك أسئلة؟ " - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"الرجاء قراءة {a_start}الأسئلة الأكثر تكراراً الخاصة بموقعنا للاطلاع على " -"الأسئلة الشائعة لما يتعلق بشهاداتنا{a_end}." - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "المتطلّبات التقنيّة" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" -"يرجى التأكد من تحديث متصفحك إلى {a_start}أحدث نسخة متاحة{a_end}، كما يرجى " -"التأكد من أن كاميرا الكومبيوتر لديك متصلة ومشغّلة ومسموح بتشغيلها " -"على متصفح الإنترنت لديك (عادة ما يمكن تنسيق هذه الإعدادات من خلال إعدادات " -"المتصفح لديك)." - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "قم بتعديل اسمك الكامل " +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "حدث الخاطئ التالي أثناء تعديل إسمك:" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -16827,362 +16707,57 @@ msgstr "إجراء عملية التحقّق الخاصة بالمساق {course msgid "Enroll In {course_name}" msgstr "التسجيل في {course_name}" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "هل لديك أسئلة؟ " + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"الرجاء قراءة {a_start}الأسئلة الأكثر تكراراً الخاصة بموقعنا للاطلاع على " +"الأسئلة الشائعة لما يتعلق بشهاداتنا{a_end}." + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "المتطلّبات التقنيّة" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" +"يرجى التأكد من تحديث متصفحك إلى {a_start}أحدث نسخة متاحة{a_end}، كما يرجى " +"التأكد من أن كاميرا الكومبيوتر لديك متصلة ومشغّلة ومسموح بتشغيلها " +"على متصفح الإنترنت لديك (عادة ما يمكن تنسيق هذه الإعدادات من خلال إعدادات " +"المتصفح لديك)." + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "إعادة التحقّق" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "لم يتم التعرف على كاميرا الويب " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." -msgstr "" -"يبدو أنه لا توجد كاميرا ويب متّصلة بالكمبيوتر. الرجاء إعادة التحقّق من توصيل" -" الكاميرا وبأنها تعمل للمتابعة." - -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "لم يتم التحقق من وجود ذاكرة فلاش " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"يبدو أنه لا يوجد لديك ذاكرة فلاش مركّبة. {a_start} احصل على ذاكرة فلاش " -"{a_end} للاستمرار في عملية التسجيل. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "حدث خطأ أثناء تقديم الصور الخاصة بك." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"عذراً! يبدو أنه قد حدث خطأٌ ما. الرجاء تأكيد التفاصيل الخاصة بك وإعادة " -"المحاولة." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" msgstr "" -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "تطورك " - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "الخطوة الحالية: " - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "إعادة التقاط صورة " - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "إعادة التقاط صورة الهوية" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "إعادة التقاط صورة لك" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"استخدم كاميرا الويب الخاصة بك لأخذ صورة لوجهك حتى نتمكن من مطابقتها مع " -"الصورة على بطاقتك الشخصية. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"لا يمكنك رؤية صورتك؟ قم بالتحقق من إمكانية استخدام متصفحك للكاميرا الخاصة بك" -" عندما يطلب الإذن منك. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "إعادة التصوير " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "تم بشكلٍ جيد " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "نصائح حول كيفية أخذ صورة ناجحة" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "قم بالتأكد من الإضاءة الجيدة على وجهك " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "قم بالتأكد من أن وجهك بالكامل داخل الإطار " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "هل يمكننا مطابقة الصورة التي أخذتها مع الصورة على بطاقتك الشخصية؟ " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "أسئلة شائعة " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "لماذا تحتاجون صورتي؟ " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "نحتاج صورتك كجزء من عملية التحقق وذلك للتأكد من هويتك. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "ماذا تفعلون بهذه الصورة؟ " - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "نقوم باستعمالها فقط للتحقق من هويتك، ولن يتم عرضها في أي مكان آخر. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "بمجرد رضاك عن جودة صورتك يمكنك الانتقال لخطوة 2. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "العودة إلى الخطوة 2: إعادة التقاط صورة الهوية" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "أرنا بطاقتك الشخصية " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"استخدم كاميرا الويب الخاصة بك لأخذ صورة لبطاقتك الشخصية حتى نستطيع مطابقتها " -"مع صورتك والاسم المستخدم على حسابك. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "قم بالتأكد من أن بطاقة هويتك مضاءة بشكل جيد " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "تحقق من عدم وجود أي لمعان أو وهج" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "تأكد من قدرتك على رؤية صورتك وقراءة اسمك " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "حاول إبقاء أصابعك على الحافة لتفادي تغطية معلومات مهمة " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "لماذا تحتاجون صورة عن بطاقة هويتي؟ " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "نحتاج لمطابقة هويتك مع صورتك وإسمك بالبطاقة الشخصية للتحقق من شخصك. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"نقوم بتشفيرها وإرسالها للمراجعة في خدمة التوثيق الآمنة التابعة لنا. نحن " -"نستعمل أقصى مستويات حماية المعلومات ولا نقوم بحفظ الصورة أو المعلومات في أي " -"مكان بمجرد استكمال عملية المطابقة. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "يمكنك الانتقال لخطوة 3 حال تأكدك من جودة صورة بطاقة هويتك. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "انتقل للخطوة 3: مراجعة معلوماتك " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "التحقق من المادة المقدّمة" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "التأكد من أننا نستطيع تأكيد هويتك من الصور والمعلومات أدناه. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "معاينة الصور التي أعدت التقاطها" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "الرجاء مراجعة الصور والتأكد من مطابقتها المتطلبات المذكورة أدناه. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "يجب أن تطابق الصورة أعلاه المتطلبات التالية: " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "أن تكون مضاءة جيداً " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "أن تظهر وجهك بالكامل " - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "أن تطابق الصورة على بطاقتك الشخصية صورة وجهك. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "أن تكون مقروءة (ليست بعيدة جداً، ودون وهج) " - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "يجب أن يطابق الاسم على بطاقتك الاسم المسجّل على حسابك أدناه " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "ماذا لو كانت الصور لا تطابق المتطلبات؟ " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "يجب إعادة أخذ الصور مرة أخرى " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "ينبغي التحقق من إسمك " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "تعديل إسمك " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"بمجرد التحقّق من كون تفاصيلك تطابق المتطلبات، بإمكانك الانتقال لتأكيد تقديم " -"متطلّبات إعادة التحقّق." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "نعم! جميع التفاصيل مطابقة. " - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "تحتاج إلى إعادة عملية التحقّق لتتمكّن من المتابعة" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"للمتابعة ضمن مسار الهويّات الموثقة في المساق {course}، يتعين عليك إعادة " -"عملية التحقق من هويتك قبل تاريخ {date}. الرجاء الذهاب إلى الرابط." - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "تأكيد تقديم متطلّبات إعادة التحقّق" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "تم تحديث بيانات التحقق من الهويَّة الخاصّة بك" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"لقد حصلنا على معلوماتك التي قمت بإعادة تقديمها وسنقوم بمراجعتها من أجل " -"التحقّق من هويّتك في أقرب وقت. سيصلك تحديث خاص بحالة تصديق شهادتك خلال يومٍ " -"أو يومين. في هذه الأثناء يمكنك الوصول إلى كافة عناصر محتوى مساقك." - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "العودة إلى لوحة المعلومات" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "لم تنجح عملية إعادة التحقق" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"تم تقديم بيانات إعادة التحقق الخاصة بك بعد الموعد النهائي المحدد لإعادة " -"التحقق، ولم يعد بالتالي بإمكانك إعادة عملية التحقق." - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" -"الرجاء الاتصال بقسم الدعم إذا كنت تعتقد بحدوث خطأ يتعلق بهذه الرسالة. " - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(مفعّل){span_end}" @@ -18865,14 +18440,14 @@ msgstr "الصفحة الرئيسية لاستوديو {studio_name}" msgid "New Course" msgstr "مساق جديد" -#: cms/templates/index.html -msgid "New Library" -msgstr "مكتبة جديدة" - #: cms/templates/index.html msgid "Email staff to create course" msgstr "مراسلة فريق العمل عبر البريد الإكتروني لإنشاء مساق" +#: cms/templates/index.html +msgid "New Library" +msgstr "مكتبة جديدة" + #: cms/templates/index.html msgid "Please correct the highlighted fields below." msgstr "يُرجى تصحيح الحقول المركَّز عليها أدناه." @@ -19583,8 +19158,6 @@ msgstr "معلومات أساسية" msgid "The nuts and bolts of your course" msgstr "مكوِّنات العمل الأساسية لمساقك" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "عذرًا، لم يعد هذا الحقل مفعّلًا: لا يمكن تغيير هذه المعلومات." @@ -19647,7 +19220,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -20443,6 +20026,10 @@ msgstr "كيفية استخدام {studio_name} لتبني مساقك" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "هل لديك أي مشاكل، أو أسئلة، أو اقتراحات بشأن {studio_name}؟ " +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "هذا نص أساسي. " diff --git a/conf/locale/ar/LC_MESSAGES/djangojs.mo b/conf/locale/ar/LC_MESSAGES/djangojs.mo index 010a09833e..44d513c48a 100644 Binary files a/conf/locale/ar/LC_MESSAGES/djangojs.mo and b/conf/locale/ar/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/ar/LC_MESSAGES/djangojs.po b/conf/locale/ar/LC_MESSAGES/djangojs.po index 023ce92cdb..ed6ed2c8fc 100644 --- a/conf/locale/ar/LC_MESSAGES/djangojs.po +++ b/conf/locale/ar/LC_MESSAGES/djangojs.po @@ -67,6 +67,7 @@ # Translators: # Abdallah Nassif , 2014 # may , 2014 +# Mohamed Ibrahim , 2015 # wd3bbas , 2014 # Nabeel El-Dughailib , 2014-2015 # Saramami.mami , 2014 @@ -74,8 +75,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-26 19:41+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Arabic (http://www.transifex.com/projects/p/edx-platform/language/ar/)\n" "MIME-Version: 1.0\n" @@ -134,23 +135,8 @@ msgstr "موافق" #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore -#, fuzzy msgid "Cancel" -msgstr "" -"#-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#\n" -"إلغاء \n" -"#-#-#-#-# underscore.po (edx-platform) #-#-#-#-#\n" -"إلغاء" - -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -#, fuzzy -msgid "This link will open in a new browser window/tab" -msgstr "" -"#-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#\n" -"سيفتح هذا الرابط في نافذة متصفّح جديدة/تبويبة جديدة\n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"سيُفتح هذا الرابط في نافذة متصفّح جديدة/تبويبة جديدة" +msgstr "إلغاء" #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js @@ -239,13 +225,8 @@ msgstr "خطأ" #: lms/templates/instructor/instructor_dashboard_2/cohort-discussions-course-wide.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-discussions-inline.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore -#, fuzzy msgid "Save" -msgstr "" -"#-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#\n" -"حفظ \n" -"#-#-#-#-# underscore.po (edx-platform) #-#-#-#-#\n" -"حفظ" +msgstr "حفظ" #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML @@ -304,6 +285,10 @@ msgstr[3] "(%(num_points)s نقطة محتملة)" msgstr[4] "(%(num_points)s نقطة محتملة)" msgstr[5] "(%(num_points)s نقاط محتملة) " +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "الإجابة:" @@ -2381,13 +2366,8 @@ msgstr "تاريخ النشر" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js #: lms/templates/edxnotes/note-item.underscore -#, fuzzy msgid "More" -msgstr "" -"#-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#\n" -"المزيد\n" -"#-#-#-#-# underscore.po (edx-platform) #-#-#-#-#\n" -"أكثر" +msgstr "المزيد" #: common/static/js/vendor/ova/catch/js/catch.js msgid "My Notes" @@ -3920,6 +3900,18 @@ msgstr "عذرًا، لم يمكن تقديم الطلب." msgid "Could not retrieve payment information" msgstr "عذرًا، لم يمكن استرجاع معلومات الدفع." +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "تأكيد" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "نأسف لحدوث خطأ. يُرجى محاولة إعادة تحميل الصفحة." @@ -4117,6 +4109,10 @@ msgstr "جاري الحذف" msgid "OpenAssessment Save Error" msgstr "نأسف لحدوث خطأ في حفظ التقييم المفتوح." +#: cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "سيُفتح هذا الرابط في نافذة متصفّح جديدة/تبويبة جديدة" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "سيفتح هذا الرابط في نافذة منبثقة جديدة." @@ -4773,13 +4769,8 @@ msgstr "تعديل تاريخ إصدار القسم" #: cms/static/js/views/overview_assignment_grader.js #: cms/templates/js/course-outline.underscore #: cms/templates/js/grading-editor.underscore -#, fuzzy msgid "Not Graded" -msgstr "" -"#-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-#\n" -"غير مقيَّم \n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"غير مقيَّم" +msgstr "غير مقيَّم" #: cms/static/js/views/paged_container.js msgid "Date added" @@ -4807,13 +4798,8 @@ msgstr "" #: cms/static/js/views/uploads.js #: cms/templates/js/metadata-file-uploader-item.underscore #: cms/templates/js/video/metadata-translations-item.underscore -#, fuzzy msgid "Upload" -msgstr "" -"#-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-#\n" -"حمّل الملف\n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"تحميل " +msgstr "حمّل الملف" #: cms/static/js/views/uploads.js msgid "We're sorry, there was an error" @@ -4885,13 +4871,8 @@ msgstr "انشر كلّ التعديلات غير المنشورة لهذه %(it #: cms/static/js/views/modals/course_outline_modals.js #: cms/templates/js/course-outline.underscore #: cms/templates/js/publish-xblock.underscore -#, fuzzy msgid "Publish" -msgstr "" -"#-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-#\n" -"انشر\n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"نشر" +msgstr "انشر" #. Translators: "title" is the name of the current component being edited. #: cms/static/js/views/modals/edit_xblock.js @@ -5577,6 +5558,45 @@ msgstr "" msgid "Forgot password?" msgstr "هل نسيت كلمة المرور؟" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "تسجيل الدخول" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5604,15 +5624,15 @@ msgstr "عذرًا، لم نتمكّن من تسجيل دخولك. " msgid "An error occurred when signing you in to %(platformName)s." msgstr "" -#: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "تسجيل الدخول" - #: lms/templates/student_account/login.underscore msgid "or sign in with" msgstr "أو تسجيل الدخول من خلال" +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "New to %(platformName)s?" msgstr "هل تستخدم %(platformName)s لأوّل مرّة؟" @@ -6055,6 +6075,22 @@ msgstr "" "رخصة القيادة، أو جواز السفر، أو بطاقة شخصية صادرة عن الحكومة، بحيث تحمل أي " "واحدة من هذه الوثائق اسمك وصورتك " +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "مراجعة صورك" @@ -6119,10 +6155,6 @@ msgstr "إعادة التقاط صورك" msgid "Before proceeding, please confirm that your details match" msgstr "قبل المتابعة، يُرجى التأكّد من تطابق بياناتك" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "تأكيد" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/eo/LC_MESSAGES/django.mo b/conf/locale/eo/LC_MESSAGES/django.mo index b2e963ee7d..feff5af0ce 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 7da5df535f..ca8cf0c129 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: 2015-06-19 13:47+0000\n" -"PO-Revision-Date: 2015-06-19 13:47:59.148161\n" +"POT-Creation-Date: 2015-06-29 12:36+0000\n" +"PO-Revision-Date: 2015-06-29 12:36:50.076432\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -809,13 +809,48 @@ msgstr "" "Ûnäßlé tö sénd émäïl äçtïvätïön lïnk. Pléäsé trý ägäïn lätér. Ⱡ'σяєм ιρѕυм " "∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "Nämé réqüïréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" +"Àüthéntïçätïön wïth {} ïs çürréntlý ünäväïläßlé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " +"αмєт, ¢σηѕє¢тєтυя α#" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "Ìnvälïd ÌD Ⱡ'σяєм ιρѕυм ∂σłσ#" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" +"Séçöndärý prövïdérs äré dïspläýéd léss prömïnéntlý, ïn ä sépäräté lïst öf " +"\"Ìnstïtütïön\" lögïn prövïdérs. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" +"Ìf thïs öptïön ïs énäßléd, üsérs wïll nöt ßé äskéd tö çönfïrm théïr détäïls " +"(nämé, émäïl, étç.) dürïng thé régïsträtïön pröçéss. Önlý séléçt thïs öptïön" +" för trüstéd prövïdérs thät äré knöwn tö prövïdé äççüräté üsér ïnförmätïön. " +"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ " +"тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм," +" qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ " +"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє " +"¢ιłłυм ∂σłσяє єυ ƒυ#" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" +"Ìf thïs öptïön ïs séléçtéd, üsérs wïll nöt ßé réqüïréd tö çönfïrm théïr " +"émäïl, änd théïr äççöünt wïll ßé äçtïvätéd ïmmédïätélý üpön régïsträtïön. " +"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ " +"тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм," +" qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ " +"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє " +"¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт " +"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂#" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1285,6 +1320,24 @@ msgstr "ünänswéréd Ⱡ'σяєм ιρѕυм ∂σłσ#" msgid "processing" msgstr "pröçéssïng Ⱡ'σяєм ιρѕυм ∂σłσ#" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "Thïs ïs çörréçt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "Thïs ïs ïnçörréçt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "Thïs ïs ünänswéréd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "Thïs ïs ßéïng pröçésséd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -2281,6 +2334,16 @@ msgstr "" "ÖRÀ1 ïs nö löngér süppörtéd. Tö üsé thïs ässéssmént, répläçé thïs ÖRÀ1 " "çömpönént wïth än ÖRÀ2 çömpönént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "TBD Ⱡ'σяєм#" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "LTÌ Pässpörts Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" @@ -3478,13 +3541,6 @@ msgstr "" msgid "General" msgstr "Généräl Ⱡ'σяєм ιρѕυм #" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "TBD Ⱡ'σяєм#" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "Dïsçüssïön Ìd Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" @@ -7322,7 +7378,6 @@ msgstr "ÌD Ⱡ'σя#" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "Füll Nämé Ⱡ'σяєм ιρѕυм ∂σł#" @@ -8284,6 +8339,7 @@ msgid "Verified Enrollment" msgstr "Vérïfïéd Énröllmént Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" #: lms/djangoapps/shoppingcart/reports.py +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue" msgstr "Gröss Révénüé Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" @@ -8328,7 +8384,6 @@ msgstr "" " ¢σηѕє¢тєтυя α#" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "" "Thé çöürsé ýöü réqüéstéd döés nöt éxïst. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " @@ -9222,19 +9277,6 @@ msgstr "" " Pössïßlé äçtïön: rétrý wïth ä dïfférént förm öf päýmént.\n" " Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє#" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "Döwnlöäd ÇSV Répörts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"Théré wäs än érrör ïn ýöür däté ïnpüt. Ìt shöüld ßé förmättéd äs ÝÝÝÝ-MM-DD" -" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -9367,7 +9409,6 @@ msgid "Payment confirmation" msgstr "Päýmént çönfïrmätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" #: lms/djangoapps/verify_student/views.py -#: lms/templates/verify_student/photo_reverification.html msgid "Take photo" msgstr "Täké phötö Ⱡ'σяєм ιρѕυм ∂σłσ#" @@ -10749,6 +10790,16 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "Ìnvälïd préréqüïsïté çöürsé kéý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" +"Àn érrör öççürréd whïlé trýïng tö sävé ýöür täßs Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " +"αмєт, ¢σηѕє¢тєтυя α#" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "Täßs Éxçéptïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -11048,7 +11099,6 @@ msgid "Loading" msgstr "Löädïng Ⱡ'σяєм ιρѕυм #" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "çlösé Ⱡ'σяєм ιρѕ#" @@ -11193,11 +11243,6 @@ msgstr "Hélp Ⱡ'σяєм ι#" msgid "Sign Out" msgstr "Sïgn Öüt Ⱡ'σяєм ιρѕυм ∂#" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "nämé Ⱡ'σяєм ι#" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "Àll Rïghts Résérvéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" @@ -11228,12 +11273,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "Sömé Rïghts Résérvéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" -"Ûpgrädé Ýöür Énröllmént för {} | Çhöösé Ýöür Träçk Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " -"αмєт, ¢σηѕє¢тєтυя α#" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -11245,6 +11284,12 @@ msgstr "" "Sörrý, théré wäs än érrör whén trýïng tö énröll ýöü Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " "αмєт, ¢σηѕє¢тєтυя α#" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" +"Çöngrätülätïöns! Ýöü äré nöw énrölléd ïn {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя " +"ѕιт αмєт, ¢σηѕє¢тєтυя #" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -14489,12 +14534,13 @@ msgstr "" "ѕιт αмєт, ¢σηѕє¢тєтυя α#" #: lms/templates/courseware/progress.html -msgid "Go to your dashboard" -msgstr "Gö tö ýöür däshßöärd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" +msgid "{link} to purchase course credit." +msgstr "" +"{link} tö pürçhäsé çöürsé çrédït. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." -msgstr "tö pürçhäsé çöürsé çrédït. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" +msgid "Go to your dashboard" +msgstr "Gö tö ýöür däshßöärd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" #: lms/templates/courseware/progress.html msgid "You have not yet met the requirements for credit." @@ -14502,6 +14548,10 @@ msgstr "" "Ýöü hävé nöt ýét mét thé réqüïréménts för çrédït. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " "αмєт, ¢σηѕє¢тєтυя α#" +#: lms/templates/courseware/progress.html +msgid "display_name" +msgstr "dïspläý_nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + #: lms/templates/courseware/progress.html msgid "Verification Submitted" msgstr "Vérïfïçätïön Süßmïttéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" @@ -14515,9 +14565,8 @@ msgid "Upcoming" msgstr "Ûpçömïng Ⱡ'σяєм ιρѕυм ∂#" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" -msgstr "Möré Ⱡ'σяєм ι#" +msgid "Less" +msgstr "Léss Ⱡ'σяєм ι#" #: lms/templates/courseware/progress.html msgid "{earned:.3n} of {total:.3n} possible points" @@ -14883,6 +14932,47 @@ msgstr "" "¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт " "ηση ρяσι∂єηт, ѕ#" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" +"{username}, ýöür élïgïßïlïtý för çrédït éxpïrés ön {expiry}. Dön't mïss öüt!" +" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" +"{congrats} {username}, Ýöü hävé méét réqüïréménts för çrédït. Ⱡ'σяєм ιρѕυм " +"∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "Pürçhäsé Çrédït Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" +"Thänk ýöü, ýöür päýmént ïs çömplété, ýöür çrédït ïs pröçéssïng. Pléäsé séé " +"{provider_link} för möré ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" +"Thänk ýöü, ýöür çrédït ïs äpprövéd. Pléäsé séé {provider_link} för möré " +"ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя#" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" +"Ýöür çrédït häs ßéén dénïéd. Pléäsé çöntäçt {provider_link} för möré " +"ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "édït Ⱡ'σяєм ι#" @@ -15395,6 +15485,10 @@ msgstr "Ûnpïn Ⱡ'σяєм ιρѕ#" msgid "Open" msgstr "Öpén Ⱡ'σяєм ι#" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "Möré Ⱡ'σяєм ι#" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -18703,10 +18797,21 @@ msgstr "Vïéw Çöürsés Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" msgid "Payment" msgstr "Päýmént Ⱡ'σяєм ιρѕυм #" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "Döwnlöäd ÇSV Répörts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "Döwnlöäd ÇSV Dätä Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" +"Théré wäs än érrör ïn ýöür däté ïnpüt. Ìt shöüld ßé förmättéd äs ÝÝÝÝ-MM-DD" +" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "" @@ -19091,14 +19196,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "{platform_name} - Shöppïng Çärt Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "Révïéw Ⱡ'σяєм ιρѕυ#" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "Çönfïrmätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" @@ -19329,138 +19430,16 @@ msgstr "" " href=\"{mail_to_link}\"\">{mail_to_link}. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢т#" -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "Édït Ýöür Nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" +#: lms/templates/verify_student/face_upload.html +msgid "Edit Your Full Name" +msgstr "Édït Ýöür Füll Nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "The following error occurred while editing your name:" msgstr "" "Thé föllöwïng érrör öççürréd whïlé édïtïng ýöür nämé: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт" " αмєт, ¢σηѕє¢тєтυя α#" -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"Tö üphöld thé çrédïßïlïtý öf {platform} çértïfïçätés, äll nämé çhängés wïll " -"ßé löggéd änd réçördéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "Réäsön för nämé çhängé: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "Çhängé mý nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" -"Whý Dö Ì Nééd tö Ré-Vérïfý Mý Ìdéntïtý? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя#" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" -"Ýöü mäý nééd tö ré-vérïfý ýöür ïdéntïtý ïf än érrör öççürs wïth ýöür " -"vérïfïçätïön ör ïf ýöür vérïfïçätïön häs éxpïréd. Àll vérïfïçätïöns éxpïré " -"äftér öné ýéär. Thé ré-vérïfïçätïön pröçéss ïs thé sämé äs thé örïgïnäl " -"vérïfïçätïön pröçéss. Ýöü nééd ä wéßçäm änd ä gövérnmént-ïssüéd phötö ÌD. " -"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ " -"тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм," -" qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ η#" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "Hävïng Téçhnïçäl Tröüßlé? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" -"Pléäsé mäké süré ýöür ßröwsér ïs üpdätéd tö thé {a_start}möst réçént" -" vérsïön pössïßlé{a_end}. Àlsö, pléäsé mäké süré ýöür " -"wéßçäm ïs plüggéd ïn, türnéd ön, änd ällöwéd tö fünçtïön ïn ýöür wéß" -" ßröwsér (çömmönlý ädjüstäßlé ïn ýöür ßröwsér séttïngs) Ⱡ'σяєм " -"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя " -"ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ " -"ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ " -"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσł#" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "Hävé qüéstïöns? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"Pléäsé réäd {a_start}öür FÀQs tö vïéw çömmön qüéstïöns äßöüt öür " -"çértïfïçätés{a_end}. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" -"Ýöü äré üpgrädïng ýöür énröllmént för: {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " -"αмєт, ¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" -"Ýöü äré ré-vérïfýïng för: {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" -"Ýöü äré énröllïng ïn: {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" -"Çöngrätülätïöns! Ýöü äré nöw énrölléd ïn {course_display} Ⱡ'σяєм ιρѕυм ∂σłσя" -" ѕιт αмєт, ¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "Téçhnïçäl Réqüïréménts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" -"Pléäsé mäké süré ýöür ßröwsér ïs üpdätéd tö thé {a_start}möst réçént vérsïön" -" pössïßlé{a_end}. Àlsö, pléäsé mäké süré ýöür wéßçäm ïs plüggéd ïn, " -"türnéd ön, änd ällöwéd tö fünçtïön ïn ýöür wéß ßröwsér (çömmönlý ädjüstäßlé " -"ïn ýöür ßröwsér séttïngs). Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя" -" α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє " -"мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ " -"łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη " -"яєρяєнєη∂єяιт ιη νσłυρтαтє νєł#" - -#: lms/templates/verify_student/face_upload.html -msgid "Edit Your Full Name" -msgstr "Édït Ýöür Füll Nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" - #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "Ré-Vérïfý för {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" @@ -19496,432 +19475,71 @@ msgstr "Vérïfý För {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт# msgid "Enroll In {course_name}" msgstr "Énröll Ìn {course_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "Hävé qüéstïöns? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"Pléäsé réäd {a_start}öür FÀQs tö vïéw çömmön qüéstïöns äßöüt öür " +"çértïfïçätés{a_end}. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "Téçhnïçäl Réqüïréménts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" +"Pléäsé mäké süré ýöür ßröwsér ïs üpdätéd tö thé {a_start}möst réçént vérsïön" +" pössïßlé{a_end}. Àlsö, pléäsé mäké süré ýöür wéßçäm ïs plüggéd ïn, " +"türnéd ön, änd ällöwéd tö fünçtïön ïn ýöür wéß ßröwsér (çömmönlý ädjüstäßlé " +"ïn ýöür ßröwsér séttïngs). Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя" +" α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє " +"мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ " +"łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη " +"яєρяєнєη∂єяιт ιη νσłυρтαтє νєł#" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "Ré-Vérïfïçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "Nö Wéßçäm Détéçtéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "Ìdéntïtý Vérïfïçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -"Ýöü dön't séém tö hävé ä wéßçäm çönnéçtéd. Döüßlé-çhéçk thät ýöür wéßçäm ïs " -"çönnéçtéd änd wörkïng tö çöntïnüé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "Nö Fläsh Détéçtéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"Ýöü dön't séém tö hävé Fläsh ïnställéd. {a_start} Gét Fläsh {a_end} tö " -"çöntïnüé ýöür régïsträtïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "Érrör süßmïttïng ýöür ïmägés Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"Ööps! Söméthïng wént wröng. Pléäsé çönfïrm ýöür détäïls änd trý ägäïn. " -"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "Vérïfý Ýöür Ìdéntïtý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." -msgstr "" -"Tö vérïfý ýöür ïdéntïtý änd çöntïnüé äs ä vérïfïéd stüdént ïn thïs çöürsé, " -"çömplété thé föllöwïng stéps {start_bold}ßéföré thé çöürsé vérïfïçätïön " -"déädlïné{end_bold}. Ìf ýöü dö nöt vérïfý ýöür ïdéntïtý, ýöü çän stïll " -"réçéïvé än hönör çödé çértïfïçäté för thé çöürsé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " -"αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт " -"łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ " -"єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ " -"αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ι#" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "Ýöür Prögréss Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "Çürrént Stép: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "Ré-Täké Phötö Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "Ré-Täké ÌD Phötö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "Ré-Täké Ýöür Phötö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"Ûsé ýöür wéßçäm tö täké ä pïçtüré öf ýöür fäçé sö wé çän mätçh ït wïth thé " -"pïçtüré ön ýöür ÌD. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"Dön't séé ýöür pïçtüré? Mäké süré tö ällöw ýöür ßröwsér tö üsé ýöür çämérä " -"whén ït äsks för pérmïssïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "Rétäké Ⱡ'σяєм ιρѕυ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "Lööks gööd Ⱡ'σяєм ιρѕυм ∂σłσ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "" -"Tïps ön täkïng ä süççéssfül phötö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "Mäké süré ýöür fäçé ïs wéll-lït Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "" -"Bé süré ýöür éntïré fäçé ïs ïnsïdé thé främé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "" -"Çän wé mätçh thé phötö ýöü töök wïth thé öné ön ýöür ÌD? Ⱡ'σяєм ιρѕυм ∂σłσя " -"ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" -"Önçé ïn pösïtïön, üsé thé çämérä ßüttön {btn_icon} tö çäptüré ýöür pïçtüré " -"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" -"Ûsé thé çhéçkmärk ßüttön {btn_icon} önçé ýöü äré häppý wïth thé phötö Ⱡ'σяєм" -" ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "Çömmön Qüéstïöns Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "Whý dö ýöü nééd mý phötö? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" -"Às pärt öf thé vérïfïçätïön pröçéss, wé nééd ýöür phötö tö çönfïrm thät ýöü " -"äré ýöü. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "" -"Whät dö ýöü dö wïth thïs pïçtüré? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" -"Wé önlý üsé ït tö vérïfý ýöür ïdéntïtý. Ìt ïs nöt dïspläýéd änýwhéré. " -"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "Vérïfïçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "" -"Önçé ýöü vérïfý ýöür phötö lööks gööd, ýöü çän mövé ön tö stép 2. Ⱡ'σяєм " -"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "Gö tö Stép 2: Ré-Täké ÌD Phötö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "Shöw Ûs Ýöür ÌD Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"Ûsé ýöür wéßçäm tö täké ä pïçtüré öf ýöür ÌD sö wé çän mätçh ït wïth ýöür " -"phötö änd thé nämé ön ýöür äççöünt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "Mäké süré ýöür ÌD ïs wéll-lït Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" -"Àççéptäßlé ÌDs ïnçlüdé drïvér's lïçénsés, pässpörts, ör öthér gövérmént-" -"ïssüéd ÌDs thät ïnçlüdé ýöür nämé änd phötö Ⱡ'σяєм ιρѕυм ∂σł#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "" -"Çhéçk thät théré ïsn't äný gläré Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "" -"Énsüré thät ýöü çän séé ýöür phötö änd réäd ýöür nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт" -" αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" -"Trý tö kéép ýöür fïngérs ät thé édgé tö ävöïd çövérïng ïmpörtänt ïnförmätïön" -" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" -"Önçé ïn pösïtïön, üsé thé çämérä ßüttön {btn_icon} tö çäptüré ýöür ÌD Ⱡ'σяєм" -" ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "" -"Whý dö ýöü nééd ä phötö öf mý ÌD? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" -"Wé nééd tö mätçh ýöür ÌD wïth ýöür phötö änd nämé tö çönfïrm thät ýöü äré " -"ýöü. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"Wé énçrýpt ït änd sénd ït tö öür séçüré äüthörïzätïön sérvïçé för révïéw. Wé" -" üsé thé hïghést lévéls öf séçürïtý änd dö nöt sävé thé phötö ör ïnförmätïön" -" änýwhéré önçé thé mätçh häs ßéén çömplétéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт " -"∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση " -"υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє " -"∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα" -" ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" -"Önçé ýöü vérïfý ýöür ÌD phötö lööks gööd, ýöü çän mövé ön tö stép 3. Ⱡ'σяєм " -"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "Gö tö Stép 3: Révïéw Ýöür Ìnfö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "Vérïfý Ýöür Süßmïssïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" -"Mäké süré wé çän vérïfý ýöür ïdéntïtý wïth thé phötös änd ïnförmätïön ßélöw." -" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "" -"Révïéw thé Phötös Ýöü'vé Ré-Täkén Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" -"Pléäsé révïéw thé phötös änd vérïfý thät théý méét thé réqüïréménts lïstéd " -"ßélöw. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "" -"Thé phötö äßövé nééds tö méét thé föllöwïng réqüïréménts: Ⱡ'σяєм ιρѕυм ∂σłσя" -" ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "Bé wéll lït Ⱡ'σяєм ιρѕυм ∂σłσя #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "Shöw ýöür whölé fäçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "" -"Thé phötö ön ýöür ÌD müst mätçh thé phötö öf ýöür fäçé Ⱡ'σяєм ιρѕυм ∂σłσя " -"ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "" -"Bé réädäßlé (nöt töö fär äwäý, nö gläré) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "" -"Thé nämé ön ýöür ÌD müst mätçh thé nämé ön ýöür äççöünt ßélöw Ⱡ'σяєм ιρѕυм " -"∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "" -"Phötös dön't méét thé réqüïréménts? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "Rétäké Ýöür Phötös Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "Çhéçk Ýöür Nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" -"Mäké süré ýöür füll nämé ön ýöür {platform_name} äççöünt ({full_name}) " -"mätçhés ýöür ÌD. Wé wïll älsö üsé thïs äs thé nämé ön ýöür çértïfïçäté. " -"Ⱡ'σяєм ιρѕ#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "Édït ýöür nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"Önçé ýöü vérïfý ýöür détäïls mätçh thé réqüïréménts, ýöü çän mövé öntö tö " -"çönfïrm ýöür ré-vérïfïçätïön süßmïsssïön. Ⱡ'σяєм ιρѕυм ∂σł#" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "Ýés! Mý détäïls äll mätçh. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "" -"Ýöü nééd tö ré-vérïfý tö çöntïnüé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"Tö çöntïnüé ïn thé ÌD Vérïfïéd träçk ïn {course}, ýöü nééd tö ré-vérïfý ýöür" -" ïdéntïtý ßý {date}. Gö tö ÛRL. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "" -"Ré-Vérïfïçätïön Süßmïssïön Çönfïrmätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя#" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "" -"Ýöür Çrédéntïäls Hävé Béén Ûpdätéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"Wé'vé çäptüréd ýöür ré-süßmïttéd ïnförmätïön änd wïll révïéw ït tö vérïfý " -"ýöür ïdéntïtý shörtlý. Ýöü shöüld réçéïvé än üpdäté tö ýöür vérïïçätïön " -"stätüs wïthïn 1-2 däýs. Ìn thé méäntïmé, ýöü stïll hävé äççéss tö äll öf " -"ýöür çöürsé çöntént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg " +"Ýöü hävé älréädý süßmïttéd ýöür vérïfïçätïön ïnförmätïön. Ýöü wïll séé ä " +"méssägé ön ýöür däshßöärd whén thé vérïfïçätïön pröçéss ïs çömplété (üsüällý" +" wïthïn 1-2 däýs). Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg " "єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт " "єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт " "αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη " -"νσłυρтαтє νєłιт єѕ#" +"νσłυρтαтє νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт " +"σ¢¢αє¢αт ¢υρι∂αтαт ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢#" -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." +msgstr "" +"Ýöü çännöt vérïfý ýöür ïdéntïtý ät thïs tïmé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "Rétürn tö Ýöür Däshßöärd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "Ré-Vérïfïçätïön Fäïléd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"Ýöür ré-vérïfïçätïön wäs süßmïttéd äftér thé ré-vérïfïçätïön déädlïné, änd " -"ýöü çän nö löngér ßé ré-vérïfïéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" -"Pléäsé çöntäçt süppört ïf ýöü ßélïévé thïs méssägé tö ßé ïn érrör. Ⱡ'σяєм " -"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(äçtïvé){span_end} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" @@ -22000,14 +21618,14 @@ msgstr "{studio_name} Hömé Ⱡ'σяєм ιρѕυм ∂#" msgid "New Course" msgstr "Néw Çöürsé Ⱡ'σяєм ιρѕυм ∂σłσ#" -#: cms/templates/index.html -msgid "New Library" -msgstr "Néw Lïßrärý Ⱡ'σяєм ιρѕυм ∂σłσя #" - #: cms/templates/index.html msgid "Email staff to create course" msgstr "Émäïl stäff tö çréäté çöürsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" +#: cms/templates/index.html +msgid "New Library" +msgstr "Néw Lïßrärý Ⱡ'σяєм ιρѕυм ∂σłσя #" + #: cms/templates/index.html msgid "Please correct the highlighted fields below." msgstr "" @@ -22890,8 +22508,6 @@ msgid "The nuts and bolts of your course" msgstr "" "Thé nüts änd ßölts öf ýöür çöürsé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "" @@ -22969,9 +22585,20 @@ msgid "Successful Proctored Exam" msgstr "Süççéssfül Pröçtöréd Éxäm Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "Pröçtöréd Éxäm {number} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" msgstr "" -"Süççéssfül Ìn Çöürsé Révérïfïçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" +"Süççéssfül Ìn-Çöürsé Révérïfïçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" +msgstr "" +"Ìn-Çöürsé Révérïfïçätïön {number} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" #: cms/templates/settings.html msgid "Access to {display_name}" @@ -23922,6 +23549,10 @@ msgstr "" "Hävé prößléms, qüéstïöns, ör süggéstïöns äßöüt {studio_name}? Ⱡ'σяєм ιρѕυм " "∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "nämé Ⱡ'σяєм ι#" + # empty msgid "This is a key string." msgstr "Thïs ïs ä kéý strïng. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.mo b/conf/locale/eo/LC_MESSAGES/djangojs.mo index f2d85f5f51..685ec88ecf 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 0d50dafee2..e39a0974a7 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: 2015-06-19 13:46+0000\n" -"PO-Revision-Date: 2015-06-19 13:47:59.519977\n" +"POT-Creation-Date: 2015-06-29 12:35+0000\n" +"PO-Revision-Date: 2015-06-29 12:36:50.361468\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -89,13 +89,6 @@ msgstr "ÖK Ⱡ'σя#" msgid "Cancel" msgstr "Çänçél Ⱡ'σяєм ιρѕυ#" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "" -"Thïs lïnk wïll öpén ïn ä néw ßröwsér wïndöw/täß Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт," -" ¢σηѕє¢тєтυя α#" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -237,6 +230,12 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "(%(num_points)s pöïnt pössïßlé) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" msgstr[1] "(%(num_points)s pöïnts pössïßlé) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" +"Thé grädïng pröçéss ïs stïll rünnïng. Réfrésh thé pägé tö séé üpdätés. " +"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "Ànswér: Ⱡ'σяєм ιρѕυм #" @@ -4163,6 +4162,19 @@ msgstr "" "Çöüld nöt rétrïévé päýmént ïnförmätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя#" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "Täké ä phötö öf ýöür ÌD Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "Révïéw ýöür ïnfö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "Çönfïrm Ⱡ'σяєм ιρѕυм #" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -4382,6 +4394,12 @@ msgstr "Délétïng Ⱡ'σяєм ιρѕυм ∂#" msgid "OpenAssessment Save Error" msgstr "ÖpénÀsséssmént Sävé Érrör Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" +"Thïs lïnk wïll öpén ïn ä néw ßröwsér wïndöw/täß Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт," +" ¢σηѕє¢тєтυя α#" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "" @@ -4724,6 +4742,9 @@ msgstr "" "<%= fileExtensions %> tö üplöäd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "ör Ⱡ'σя#" @@ -6058,6 +6079,55 @@ msgstr "" msgid "Forgot password?" msgstr "Förgöt pässwörd? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "Sïgn ïn Ⱡ'σяєм ιρѕυм #" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" +"Wöüld ýöü lïké tö sïgn ïn üsïng ýöür %(providerName)s çrédéntïäls? Ⱡ'σяєм " +"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "Sïgn ïn üsïng %(providerName)s Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" +"Shöw mé öthér wäýs tö sïgn ïn ör régïstér Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" +"Sïgn ïn wïth Ìnstïtütïön/Çämpüs Çrédéntïäls Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" +"Çhöösé ýöür ïnstïtütïön fröm thé lïst ßélöw: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "Bäçk tö sïgn ïn Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" +"Régïstér wïth Ìnstïtütïön/Çämpüs Çrédéntïäls Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "Régïstér thröügh édX Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -6096,15 +6166,17 @@ msgstr "" "Àn érrör öççürréd whén sïgnïng ýöü ïn tö %(platformName)s. Ⱡ'σяєм ιρѕυм " "∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" -#: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "Sïgn ïn Ⱡ'σяєм ιρѕυм #" - #: lms/templates/student_account/login.underscore msgid "or sign in with" msgstr "ör sïgn ïn wïth Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" +msgstr "" +"Ûsé mý ïnstïtütïön/çämpüs çrédéntïäls Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυ#" + #: lms/templates/student_account/login.underscore msgid "New to %(platformName)s?" msgstr "Néw tö %(platformName)s? Ⱡ'σяєм ιρѕυм ∂σłσя #" @@ -6635,6 +6707,31 @@ msgstr "" "À drïvér's lïçénsé, pässpört, ör gövérnmént-ïssüéd ÌD wïth ýöür nämé änd " "phötö. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" +"Ìdéntïtý Vérïfïçätïön Ìn Prögréss Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" +"Wé hävé réçéïvéd ýöür ïnförmätïön änd äré vérïfýïng ýöür ïdéntïtý. Ýöü wïll " +"séé ä méssägé ön ýöür däshßöärd whén thé vérïfïçätïön pröçéss ïs çömplété " +"(üsüällý wïthïn 1-2 däýs). Ìn thé méäntïmé, ýöü çän stïll äççéss äll " +"äväïläßlé çöürsé çöntént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя " +"α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα" +" αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ " +"ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη " +"яєρяєнєη∂єяιт ιη νσłυρтαтє#" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "Rétürn tö Ýöür Däshßöärd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "Révïéw Ýöür Phötös Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" @@ -6708,10 +6805,6 @@ msgstr "" "Béföré pröçéédïng, pléäsé çönfïrm thät ýöür détäïls mätçh Ⱡ'σяєм ιρѕυм ∂σłσя" " ѕιт αмєт, ¢σηѕє¢тєтυя α#" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "Çönfïrm Ⱡ'σяєм ιρѕυм #" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/es_419/LC_MESSAGES/django.mo b/conf/locale/es_419/LC_MESSAGES/django.mo index fa658dfc7d..b5a9f0104b 100644 Binary files a/conf/locale/es_419/LC_MESSAGES/django.mo and b/conf/locale/es_419/LC_MESSAGES/django.mo differ diff --git a/conf/locale/es_419/LC_MESSAGES/django.po b/conf/locale/es_419/LC_MESSAGES/django.po index a24780389c..8f0bda7401 100644 --- a/conf/locale/es_419/LC_MESSAGES/django.po +++ b/conf/locale/es_419/LC_MESSAGES/django.po @@ -168,7 +168,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" "PO-Revision-Date: 2015-06-15 22:43+0000\n" "Last-Translator: Cristian Salamea \n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/projects/p/edx-platform/language/es_419/)\n" @@ -326,13 +326,8 @@ msgstr "Verificado" #: common/djangoapps/course_modes/models.py #: lms/templates/dashboard/_dashboard_course_listing.html -#, fuzzy msgid "ID Verified Ribbon/Badge" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"ID de la insignia o medalla verificada\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"ID verificador Ribbon / insignia" +msgstr "ID de la insignia o medalla verificada" #: common/djangoapps/course_modes/models.py msgid "You're enrolled as an honor code student" @@ -341,13 +336,8 @@ msgstr "Usted está inscrito como estudiante de código de honor" #: common/djangoapps/course_modes/models.py lms/djangoapps/branding/api.py #: openedx/core/djangoapps/user_api/views.py #: lms/templates/static_templates/honor.html -#, fuzzy msgid "Honor Code" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Codigo de Honor\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Código de Honor" +msgstr "Código de Honor" #: common/djangoapps/course_modes/models.py msgid "You're auditing this course" @@ -642,15 +632,15 @@ msgstr "El código de idioma ISO 639-1 para este idioma." #: common/djangoapps/student/models.py msgid "Namespace of enrollment attribute e.g. credit" -msgstr "" +msgstr "Espacio de nombres del atributo de inscripción. ej: credit" #: common/djangoapps/student/models.py msgid "Name of the enrollment attribute e.g. provider_id" -msgstr "" +msgstr "Nombre del artributo de inscripción. ej: provider_id" #: common/djangoapps/student/models.py msgid "Value of the enrollment attribute e.g. ASU" -msgstr "" +msgstr "Valor del atributo de inscripción. ej: ASU" #: common/djangoapps/student/views.py msgid "Course id not specified" @@ -857,19 +847,44 @@ msgstr "" "Imposible enviar el email de activación. Por favor intentelo de nuevo más " "tarde." -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "Nombre requerido" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "La autenticación con {} no está disponible en el momento." -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "ID inválido" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" +"Los proveedores secundarios se muestran con menos enfasis, en una lista " +"separada de proveedores de inicio de sesión para la \"institución\"." + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" +"Si esta opción está habilitada, no se pedirá a los usuarios que confirmen " +"sus datos (nombre, correo electrónico, etc.) durante el proceso de registro." +" Seleccione esta opción solo para proveedores de inicio de sesión de " +"confianza, que entreguen información precisa de los usuarios." + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" +"Si se selecciona esta opción, los usuarios no tendrán que confirmar su " +"correo, y sus cuentas estarán activadas inmediatamente después del registro." #: common/djangoapps/third_party_auth/pipeline.py msgid "" "This account has not yet been activated. An activation email has been re-" "sent to {email_address}." msgstr "" +"Esta cuenta no ha sido activada. El correo de activación ha sido re enviado " +"a {email_address}." #. Translators: the translation for "LONG_DATE_FORMAT" must be a format #. string for formatting dates in a long form. For example, the @@ -1315,6 +1330,24 @@ msgstr "sin responder" msgid "processing" msgstr "procesando" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "Esto es correcto." + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "Esto es incorrecto." + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "Esto no ha sido respondido." + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "Esto está siendo procesado." + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -1329,6 +1362,8 @@ msgstr "Respuesta recibida" #: common/lib/capa/capa/inputtypes.py msgid "Expected a or tag; got {given_tag} instead" msgstr "" +"Se esperaba una etiqueta o ; pero en lugar se obtuvo " +"una etiqueta {given_tag}" #: common/lib/capa/capa/inputtypes.py msgid "" @@ -1394,11 +1429,13 @@ msgid "Expected a {expected_tag} tag; got {given_tag} instead" msgstr "" "Esperaba una etiqueta {expected_tag} pero recibí una de tipo {given_tag}" +#: common/lib/capa/capa/responsetypes.py #: lms/templates/combinedopenended/open_ended_result_table.html #: lms/templates/combinedopenended/openended/open_ended_evaluation.html msgid "Correct" -msgstr "Corregir" +msgstr "Correcto" +#: common/lib/capa/capa/responsetypes.py #: lms/templates/combinedopenended/open_ended_result_table.html #: lms/templates/combinedopenended/openended/open_ended_evaluation.html msgid "Incorrect" @@ -1957,13 +1994,13 @@ msgstr "" #: common/lib/xmodule/xmodule/capa_base.py msgid "Hint: " -msgstr "" +msgstr "Consejo:" #. Translators: e.g. "Hint 1 of 3" meaning we are showing the first of three #. hints. #: common/lib/xmodule/xmodule/capa_base.py msgid "Hint ({hint_num} of {hints_count}): " -msgstr "" +msgstr "Consejo ({hint_num} de {hints_count}):" #. Translators: 'closed' means the problem's due date has passed. You may no #. longer attempt to solve the problem. @@ -2214,6 +2251,18 @@ msgid "" "ORA1 is no longer supported. To use this assessment, replace this ORA1 " "component with an ORA2 component." msgstr "" +"ORA1 ya no está soportado. Para usar este tipo de evaluación, reemplace este" +" componente ORA1 con un componente ORA2." + +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "No definida" #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" @@ -3246,13 +3295,6 @@ msgstr "" msgid "General" msgstr "General" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "No definida" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "ID de Discusión" @@ -4939,13 +4981,8 @@ msgstr "Políticas de Accesibilidad" #. in order to register a new account. #: lms/djangoapps/branding/api.py openedx/core/djangoapps/user_api/views.py #: cms/templates/widgets/footer.html lms/templates/static_templates/tos.html -#, fuzzy msgid "Terms of Service" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Términos de Servicio\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Términos del servicio" +msgstr "Términos de Servicio" #: lms/djangoapps/branding/api.py msgid "Download the {platform_name} mobile app from the Apple App Store" @@ -4965,13 +5002,15 @@ msgstr "Debe ser un tutor CCX para acceder a esta vista." #: lms/djangoapps/ccx/views.py msgid "You must be the coach for this ccx to access this view" -msgstr "" +msgstr "Debe ser el tutor para este CCX para acceder a esta vista." #: lms/djangoapps/ccx/views.py msgid "" "You cannot create a CCX from a course using a deprecated id. Please create a" " rerun of this course in the studio to allow this action." msgstr "" +"No puede crear un CCX desde un curso usando un id caducado. Por favor cree " +"una nueva versión del curso en studio para poder realizar esta acción." #: lms/djangoapps/certificates/badge_handler.py msgid "" @@ -5316,7 +5355,7 @@ msgstr "Certificados de validación" #: lms/templates/staff_problem_info.html #: lms/templates/instructor/instructor_dashboard_2/metrics.html msgid "Username" -msgstr "Nombre de usuario" +msgstr "nombre de usuario" #: lms/djangoapps/class_dashboard/dashboard_data.py #: lms/templates/instructor/instructor_dashboard_2/metrics.html @@ -5529,7 +5568,7 @@ msgstr "Tu certificado estará disponible cuando apruebes el curso." #: lms/djangoapps/courseware/views.py msgid "Certificate has already been created." -msgstr "" +msgstr "El certificado ya ha sido creado." #: lms/djangoapps/courseware/views.py msgid "Certificate is being created." @@ -5717,13 +5756,8 @@ msgid "Courses loaded in the modulestore" msgstr "Cursos cargados en la bodega de módulos" #: lms/djangoapps/dashboard/sysadmin.py lms/templates/tracking_log.html -#, fuzzy msgid "username" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Nombre de usuario\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"nombre de usuario" +msgstr "Nombre de usuario" #: lms/djangoapps/dashboard/sysadmin.py msgid "email" @@ -5920,18 +5954,13 @@ msgstr "El equipo de {platform_name}" #: lms/djangoapps/instructor/paidcourse_enrollment_report.py #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Course Staff" -msgstr "Personal de apoyo del curso" +msgstr "Equipo del curso" #: lms/djangoapps/instructor/paidcourse_enrollment_report.py #: lms/templates/courseware/course_navigation.html #: lms/templates/discussion/_underscore_templates.html -#, fuzzy msgid "Staff" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Equipo del curso\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Equipo" +msgstr "Equipo" #: lms/djangoapps/instructor/paidcourse_enrollment_report.py msgid "Used Registration Code" @@ -6183,6 +6212,7 @@ msgstr "" "El archivo debe contener una columna con el 'nombre de usuario', una columna" " con eñ 'email', o ambos" +#: lms/djangoapps/instructor/views/api.py #: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html @@ -6191,11 +6221,11 @@ msgstr "Código de cupón" #: lms/djangoapps/instructor/views/api.py msgid "Course Id" -msgstr "" +msgstr "Id de Curso" #: lms/djangoapps/instructor/views/api.py msgid "% Discount" -msgstr "" +msgstr "% de Descuento" #: lms/djangoapps/instructor/views/api.py lms/djangoapps/shoppingcart/pdf.py #: lms/djangoapps/shoppingcart/reports.py @@ -6206,6 +6236,7 @@ msgstr "" msgid "Description" msgstr "Descripción" +#: lms/djangoapps/instructor/views/api.py #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html #: lms/templates/instructor/instructor_dashboard_2/edit_coupon_modal.html msgid "Expiration Date" @@ -6213,19 +6244,19 @@ msgstr "Fecha de expiración" #: lms/djangoapps/instructor/views/api.py msgid "Is Active" -msgstr "" +msgstr "Activo" #: lms/djangoapps/instructor/views/api.py msgid "Code Redeemed Count" -msgstr "" +msgstr "Conteo de códigos redimidos" #: lms/djangoapps/instructor/views/api.py msgid "Total Discounted Seats" -msgstr "" +msgstr "Total de sillas descontadas" #: lms/djangoapps/instructor/views/api.py msgid "Total Discounted Amount" -msgstr "" +msgstr "Cantidad Total descontada" #: lms/djangoapps/instructor/views/api.py msgid "" @@ -6252,6 +6283,8 @@ msgid "" "Your executive summary report is being created. To view the status of the " "report, see the 'Pending Instructor Tasks' section." msgstr "" +"Su resumen ejecutivo está siendo generado. Puede ver el estado de la tarea " +"de generación en la sección de 'Tareas Pendientes de Instructor'." #: lms/djangoapps/instructor/views/api.py msgid "" @@ -6260,6 +6293,10 @@ msgid "" "report will be available for download in the table below. You will be able " "to download the report when it is complete." msgstr "" +"Una tarea de resumen ejecutivo ya está en progreso. Verifique la tabla de " +"'Tareas pendientes de instructor' para ver el estado de esta tarea. Cuando " +"se haya completado, el reporte estará disponible para ser descargado en la " +"tabla a continuación." #: lms/djangoapps/instructor/views/api.py msgid "Could not parse amount as a decimal" @@ -6615,7 +6652,6 @@ msgstr "ID" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "Nombre Completo" @@ -7073,13 +7109,8 @@ msgstr "" "Este componente hace referencia a un grupo de contenido inválido o borrado." #: lms/djangoapps/notes/views.py lms/templates/notes.html -#, fuzzy msgid "My Notes" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Mis notas\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Mis Anotaciones" +msgstr "Mis notas" #: lms/djangoapps/open_ended_grading/staff_grading_service.py msgid "" @@ -7473,7 +7504,8 @@ msgstr "Curso" msgid "Course Announce Date" msgstr "Fecha de anuncio del curso" -#: lms/djangoapps/shoppingcart/reports.py +#: lms/djangoapps/shoppingcart/reports.py cms/templates/settings.html +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Course Start Date" msgstr "Fecha de inicio del curso" @@ -7502,6 +7534,7 @@ msgid "Verified Enrollment" msgstr "Inscripciones verificadas" #: lms/djangoapps/shoppingcart/reports.py +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue" msgstr "Ingresos brutos" @@ -7543,7 +7576,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "Debe iniciar sesión para añadir elementos al carrito de compras" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "El curso que ha solicitado no existe" @@ -8355,19 +8387,6 @@ msgstr "" "Posible solución: intente nuevamente con otra forma de pago.\n" " " -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "Descargar reporte CSV" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"Ha habido un error en la fecha ingresada. Se debe ingresar con el formato " -"AAAA-MM-DD" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -8478,7 +8497,6 @@ msgid "Payment confirmation" msgstr "Confirmación del pago" #: lms/djangoapps/verify_student/views.py -#: lms/templates/verify_student/photo_reverification.html msgid "Take photo" msgstr "Tomar foto" @@ -8649,13 +8667,8 @@ msgstr "Tipo de curso" #: lms/envs/devstack.py #: lms/templates/instructor/instructor_dashboard_2/course_info.html -#, fuzzy msgid "Honor" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Honor\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Código de Honor" +msgstr "Honor" #: lms/templates/main_django.html cms/templates/base.html #: lms/templates/main.html @@ -9749,6 +9762,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "Clave inválida del curso prerrequisito" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "Ocurrió un error al tratar de guardar sus pestañas" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "Excepción en pestañas" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "Esta configuración de grupo esta en uso y no puede ser borrada." @@ -10021,7 +10042,6 @@ msgid "Loading" msgstr "Cargando" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "cerrar" @@ -10115,7 +10135,7 @@ msgstr "Preferencia de idioma" #: cms/templates/settings.html #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Course End Date" -msgstr "" +msgstr "Fecha de finalización del curso" #: cms/templates/settings.html lms/templates/courseware/course_about.html msgid "Requirements" @@ -10161,11 +10181,6 @@ msgstr "Ayuda" msgid "Sign Out" msgstr "Cerrar sesión" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "Todos los Derechos Reservados" @@ -10195,10 +10210,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "Algunos derechos reservados" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "Actualice su inscripción para {} | Elegir su camino" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "Inscribirse en {} | Elegir su camino" @@ -10207,6 +10218,10 @@ msgstr "Inscribirse en {} | Elegir su camino" msgid "Sorry, there was an error when trying to enroll you" msgstr "Lo sentimos, hubo un error al intentar inscribirlo" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "Felicitaciones! Ya se encuentra inscrito en {course_name}" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "Aplicar al crédito académico con un Certificado Verificado" @@ -10849,7 +10864,7 @@ msgstr "Su correo electrónico o contraseña son incorrectos" #: lms/templates/login.html msgid "An error occurred when signing you in to {platform_name}." -msgstr "" +msgstr "Ocurrió un error al iniciar su sesión en {platform_name}." #: lms/templates/login.html msgid "" @@ -11062,7 +11077,7 @@ msgstr "Tu respuesta" #: lms/templates/problem.html msgid "Hint" -msgstr "" +msgstr "Consejo" #: lms/templates/problem.html lms/templates/shoppingcart/shopping_cart.html msgid "Reset" @@ -12125,7 +12140,7 @@ msgstr "Descargar calificaciones de estudiantes" #: lms/templates/certificates/_accomplishment-banner.html msgid "Take this with you:" -msgstr "" +msgstr "Tenga con usted:" #: lms/templates/certificates/_accomplishment-banner.html msgid "Add to Mozilla Backpack" @@ -12137,11 +12152,11 @@ msgstr "Imprimir certificado" #: lms/templates/certificates/_accomplishment-banner.html msgid "Add to LinkedIn Profile" -msgstr "" +msgstr "Agregar al Perfil de LinkedIn" #: lms/templates/certificates/_accomplishment-header.html msgid "{platform_name} Home" -msgstr "" +msgstr "Inicio de {platform_name}" #: lms/templates/certificates/_accomplishment-rendering.html msgid "Noted by" @@ -13086,6 +13101,9 @@ msgid "" "You can now download your certificate as a PDF. If you keep working and " "receive a higher grade, you can request an updated certificate." msgstr "" +"Ahora puede descargar su certificado como PDF. Si continua trabajando y " +"recibe una calificación superior, puede solicitar una actualización del " +"certificado." #: lms/templates/courseware/progress.html msgid "View certificate in a new browser window or tab." @@ -13133,44 +13151,48 @@ msgstr "Solicitar certificado" #: lms/templates/courseware/progress.html msgid "Requirements for Course Credit" -msgstr "" +msgstr "Requisitos para obtener Crédito del curso" #: lms/templates/courseware/progress.html msgid "You are no longer eligible for this course." -msgstr "" +msgstr "Usted ya no es elegible para este curso." #: lms/templates/courseware/progress.html msgid "You have met the requirements for credit in this course." -msgstr "" +msgstr "Ha completado los requerimientos para obtener crédito por este curso" + +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "visite {link} para comprar crédito del curso. " #: lms/templates/courseware/progress.html msgid "Go to your dashboard" -msgstr "" - -#: lms/templates/courseware/progress.html -msgid "to purchase course credit." -msgstr "" +msgstr "Ir al panel principal" #: lms/templates/courseware/progress.html msgid "You have not yet met the requirements for credit." msgstr "" +"No ha completado los requerimientos para obtener crédito por este curso." + +#: lms/templates/courseware/progress.html +msgid "display_name" +msgstr "display_name" #: lms/templates/courseware/progress.html msgid "Verification Submitted" -msgstr "" +msgstr "Verificación enviada" #: lms/templates/courseware/progress.html msgid "Verification Failed" -msgstr "" +msgstr "La verificación falló" #: lms/templates/courseware/progress.html msgid "Upcoming" -msgstr "" +msgstr "Próximo" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" -msgstr "Más" +msgid "Less" +msgstr "Menos" #: lms/templates/courseware/progress.html msgid "{earned:.3n} of {total:.3n} possible points" @@ -13479,6 +13501,47 @@ msgstr "" "{contact_link_end} para solicitar el pago, o puede " "{unenroll_link_start}abular la matrícula {unenroll_link_end} de este curso" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" +"{username}, su elegibilidad para obtener crédito expira el {expiry}. No " +"pierda esta oportunidad!" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" +"{congrats} {username}, Ha completado los requerimientos para obtener crédito" +" por el curso." + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "Comprar crédito" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" +"Gracias, su pago ha sido completado, su crédito está siendo procesado. Por " +"favor visite {provider_link} para más información." + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" +"Gracias, su crédito fue aprobado. Por favor visite {provider_link} para más" +" información." + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" +"Su crédito ha sido negado. Por favor contacte {provider_link} para más " +"información." + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "editar" @@ -13968,6 +14031,10 @@ msgstr "Desmarcar" msgid "Open" msgstr "Abrir" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "Más" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -15057,7 +15124,7 @@ msgstr "Espectador" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Professional" -msgstr "" +msgstr "Profesional" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Basic Course Information" @@ -15377,19 +15444,21 @@ msgstr "Reenviar factura" msgid "" "Create a .csv file that contains enrollment information for your course." msgstr "" +"Crear un archivo .csv con toda la información de inscripciones para este " +"curso." #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html msgid "Create Enrollment Report" -msgstr "" +msgstr "Crear reporte de inscripciones" #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html msgid "" "Create an HTML file that contains an executive summary for this course." -msgstr "" +msgstr "Crear un archivo HTML con toda el resumen ejecutivo para este curso." #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html msgid "Create Executive Summary" -msgstr "" +msgstr "Crear resumen ejecutivo" #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html msgid "Available Reports" @@ -15568,91 +15637,91 @@ msgstr "Actualizarcódigo de cupón" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Executive Summary for {display_name}" -msgstr "" +msgstr "Resumen ejecutivo para el curso {display_name}" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Report Creation Date" -msgstr "" +msgstr "Fecha de Creación del reporte" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of Seats" -msgstr "" +msgstr "Número de sillas" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of Enrollments" -msgstr "" +msgstr "Número de inscripciones" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue Collected" -msgstr "" +msgstr "Ingresos brutos recaudados" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue Pending" -msgstr "" +msgstr "Ingresos brutos pendientes" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of Enrollment Refunds" -msgstr "" +msgstr "Número de reenvolsos de inscripción" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Amount Refunded" -msgstr "" +msgstr "Valor reembolsado" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Average Price per Seat" -msgstr "" +msgstr "Precio promedio por silla" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Frequently Used Coupon Codes" -msgstr "" +msgstr "Códigos de cupón usados frecuentemente" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of seats purchased using coupon codes" -msgstr "" +msgstr "Número de sillas adquiridas con códigos de cupón" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Rank" -msgstr "" +msgstr "Rango" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Percent Discount" -msgstr "" +msgstr "Porcentaje de descuento" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Times Used" -msgstr "" +msgstr "Número de usos" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Bulk and Single Seat Purchases" -msgstr "" +msgstr "Compras de sillas masivas e individuales para el curso" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of seats purchased individually" -msgstr "" +msgstr "Número de sillas adquiridas individualmente" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of seats purchased in bulk" -msgstr "" +msgstr "Número de sillas adquiridas de forma masiva" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Number of seats purchased with invoices" -msgstr "" +msgstr "Número de sillas adquiridas con factura" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Unused bulk purchase seats (revenue at risk)" -msgstr "" +msgstr "Sillas compradas masivamente y no usadas (ingresos en riesgo)" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Percentage of seats purchased individually" -msgstr "" +msgstr "Porcentaje de sillas adquiridas individualmente" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Percentage of seats purchased in bulk" -msgstr "" +msgstr "Porcentaje de sillas adquiridas de forma masiva" #: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Percentage of seats purchased with invoices" -msgstr "" +msgstr "Porcentaje de sillas adquiridas con factura" #: lms/templates/instructor/instructor_dashboard_2/extensions.html msgid "Individual due date extensions" @@ -15919,7 +15988,7 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Therefore, please give enough detail to account for this action." -msgstr "" +msgstr "Por favor suministre los detalles suficientes para esta acción." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Reason" @@ -16876,10 +16945,21 @@ msgstr "Ver los cursos" msgid "Payment" msgstr "Pago" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "Descargar reporte CSV" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "Descarga de Datos CSV" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" +"Ha habido un error en la fecha ingresada. Se debe ingresar con el formato " +"AAAA-MM-DD" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "Estos reportes están delimitados por fecha de inicio y finalización." @@ -17227,14 +17307,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "{platform_name} - Carrito de compras" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "Revisión" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "Confirmación" @@ -17328,11 +17404,11 @@ msgstr "Actualmente los servidores de {platform_name} están sobrecargados" #: lms/templates/student_account/finish_auth.html msgid "Please Wait" -msgstr "" +msgstr "Por favor espere" #: lms/templates/student_account/finish_auth.html msgid "Please wait" -msgstr "" +msgstr "Por favor espere" #: lms/templates/student_account/login_and_register.html msgid "Sign in or Register" @@ -17430,120 +17506,14 @@ msgstr "" "Si tiene preguntas sobre este curso o formulario, puede contactar a {mail_to_link}." -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "Edite su nombre" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "Ocurrió el siguiente error en la edición de su nombre:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"Para garantizar la credibilidad de los certificados de la plataforma " -"{platform}, todos los cambios de nombres serán registrados y almacenados." - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "Razón del cambio de nombre:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "Cambiar mi nombre" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "¿Por que debo realizar nuevamente la verificación de identidad?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" -"Debe realizar la verificación de identidad nuevamente si ocurre un error con" -" su verificación o si esta ha expirado. Todas las verificaciones expiran " -"después de un año. El proceso de re verificación es el mismo que el de " -"verificación inicial. Necesitará una cámara web y un documento oficial de " -"identificación." - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "¿Tiene inconvenientes técnicos?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" -"Por favor asegúrese de que su navegador este actualizado a la " -"{a_start}versión más reciente posible{a_end}. También " -"asegúrese de que su cámara web este conectada, encendida y permita " -"funcionar en su navegador (normalmente esto se configura en los " -"ajustes del navegador)." - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "¿Tiene preguntas?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"Por favor lea {a_start}nuestra sección de Preguntas comunes para aclarar " -"dudas sobre nuestros certificados{a_end}." - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "Está actualizando su inscripción para: {course_name}" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "Se está reverificando para: {course_name}" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "Se está inscribiendo en el curso: {course_name}" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "Felicitaciones! Ya se encuentra inscrito en {course_display}" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "Requerimientos Técnicos" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" -"Por favor asegúrese de que su navegador este actualizado a la " -"{a_start}versión más reciente posible{a_end}. También asegúrese de que " -"su cámara web este conectada, encendida y permita funcionar en su " -"navegador (normalmente esto se configura en los ajustes del " -"navegador)." - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "Edite su nombre completo" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "Ocurrió el siguiente error en la edición de su nombre:" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "Re-verificando el curso {course_name}" @@ -17576,399 +17546,61 @@ msgstr "Verificar para {course_name}" msgid "Enroll In {course_name}" msgstr "Inscribirse en {course_name}" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "¿Tiene preguntas?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"Por favor lea {a_start}nuestra sección de Preguntas comunes para aclarar " +"dudas sobre nuestros certificados{a_end}." + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "Requerimientos Técnicos" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" +"Por favor asegúrese de que su navegador este actualizado a la " +"{a_start}versión más reciente posible{a_end}. También asegúrese de que " +"su cámara web este conectada, encendida y permita funcionar en su " +"navegador (normalmente esto se configura en los ajustes del " +"navegador)." + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "Re-Verificación" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "No se detectó una cámara web." +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "Verificación de identidad" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -"Al parecer no cuenta con una cámara web conectada. Verifique que su cámara " -"web esté bien conectada y funcione adecuadamente para continuar." +"Ya ha enviado su información de verificación de identidad. Recibirá un " +"mensaje en su Panel principal cuando el proceso de verificación esté " +"completado (usualmente entre 1-2 días)." -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "No se detectó Flash" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." +msgstr "No puede verificar su identidad en este momento." -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"Usted no tiene Flash instalado. {a_start} Obtenga Flash {a_end} para poder " -"continuar con su registro." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "Error al enviar sus imágenes" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"Ups! Algo falló! Por favor confirme sus datos detallados e intente " -"nuevamente." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "Verifique su identidad" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." -msgstr "" -"Para verificar su identidad y continuar con el curso, complete los pasos " -"siguientes, {start_bold}antes de la fecha límite{end_bold}. Si no realiza la" -" verificación de identidad, solo podrá recibir un certificado de Código de " -"Honor para este curso." - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "Su progreso" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "Paso actual:" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "Volver a tomar la foto" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "Volver a tomar la foto de identificación" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "Volver a tomar su foto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"Use su cámara web para tomar una fotografía de su rostro de modo que podamos" -" verificarla contra la fotografía de su documento de identificación." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"¿No puede ver su foto? Asegúrese de permitir a su navegador que utilice la " -"cámara web cuando este le solicite tal autorización." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "Tomar nuevamente" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "Luce bien!" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "Consejos para tomar una foto exitosamente" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "Asegurese de que su rostro esté bien iluminado" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "Verifique que su cara está completamente dentro del marco de la foto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "" -"¿Podemos verificar la foto que usted acaba de tomar contra la foto en su " -"documento de identificación?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" -"Una vez en posición, use el botón de la cámara {btn_icon} para capturar la " -"imagen" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" -"Una vez esté satisfecho con la fotografía, utilice el botón de verificación " -"{btn_icon}" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "Preguntas comunes" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "¿Para que necesitan mi foto?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" -"Como parte del proceso de verificación, se requiere su fotografía para " -"confirmar su identidad." - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "¿Que harán con esta fotografía?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" -"Solo usaremos la foto para verificar su identidad. No será mostrada en " -"ningún lugar." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "Comprobación" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "Una vez haya verificado que su foto luce bien, puede pasar al paso 2." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "Ir al paso 2: volver a tomar la foto de identificación" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "Muéstrenos su documento de identidad" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"Use su cámara web para tomar una foto de su documento de identidad para que " -"podamos compararla con su foto y con el nombre de su cuenta." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "Asegurese que su documento está bien iluminado" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" -"Los documentos de identidad aceptables incluyen licencias de conducción, " -"pasaportes, o otros documentos de identificación emitidos por el gobierno " -"que tengan su nombre y su fotografía." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "Verifique que no haya brillos en el documento." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "" -"Asegurese de que se pueda ver su cara y leer su nombre en la foto del " -"documento de identificación." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" -"Intente mantener sus dedos en los bordes para evitar cubrir información " -"importante" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" -"Una vez en posición, use el botón de la cámara {btn_icon} para capturar su " -"ID" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "¿Para que necesitan una foto de mi documento de identificación?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" -"Necesitamos comparar la foto de su documento con su fotografía para validar " -"su identidad." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"Nosotros encriptamos y enviamos la información a nuestro servicio seguro de " -"autorizaciones para su revisión. Usamos los mas altos niveles de seguridad y" -" no almacenamos la fotografía o la información en ningún lugar una vez la " -"verificación ha sido terminada." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" -"Una vez haya verificado que la foto de su documento de identificación luce " -"bien, puede proceder al paso 3." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "Ir al paso 3. Revisar su información" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "Verificar su envío" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" -"Asegúrese de que podamos verificar su identidad con las imágenes y la " -"información a continuación." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "Revisar las fotos que ha tomado nuevamente" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" -"Por favor revise las fotos y verifique que cumplen con los requisitos " -"listados a continuación." - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "Las fotos deben cumplir con los siguientes requerimientos;" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "Estar bien iluminadas" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "Mostrar su rostro completo" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "La foto de su documento debe coincidir con la foto de su cara." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "Ser legibles. (No demasiado lejos, sin brillos)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "El nombre en su documento debe coincidir con el nombre en su cuenta." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "¿Sus fotos no cumplen los requerimientos?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "Tome nuevamente sus fotos" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "Verifique su nombre" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" -"Asegurese que su nombre completo en su cuenta de " -"{platform_name}({full_name}) corresponde al de su documento de " -"identificación. Este será el nombre que usaremos en sus certificados." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "Editar su nombre" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"Una vez haya verificado que las fotos cumplen con los requerimientos, puede " -"pasar a confirmar su envío de re-verificación." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "Si! toda mi información está correcta." - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "Debe re verificar su identidad para continuar" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"Para continuar en la ruta de identidad verificada para el curso {course}, " -"necesita re verificar su identidad antes de {date}. Ir a la URL." - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "Confirmación de envío de Re-Verificación" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "Sus credenciales han sido actualizadas" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"Hemos capturado la información enviada y la revisaremos para verificar su " -"identidad con prontitud. Debería recibir una actualización de su estado de " -"verificación en 1-2 días. Durante este tiempo, igualmente tendrá acceso a " -"todo el contenido de su curso." - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "Volver al panel principal" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "El proceso de re-verificación falló" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"Su re verificación fue enviada después de la fecha límite y ya no puede ser " -"realizada." - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" -"Por favor contacte al equipo de soporte, si considera que este mensaje se ha" -" producido por error." - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(activo){span_end}" @@ -19782,14 +19414,14 @@ msgstr "Página de inicio de {studio_name}" msgid "New Course" msgstr "Nuevo curso" -#: cms/templates/index.html -msgid "New Library" -msgstr "Nueva librería" - #: cms/templates/index.html msgid "Email staff to create course" msgstr "Escribanos un correo electrónico para la creación del curso" +#: cms/templates/index.html +msgid "New Library" +msgstr "Nueva librería" + #: cms/templates/index.html msgid "Please correct the highlighted fields below." msgstr "Por favor corrija los campos resaltados a continuación." @@ -20543,8 +20175,6 @@ msgstr "Información básica" msgid "The nuts and bolts of your course" msgstr "Las herramientas para la construcción de su curso" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "Este campo está deshabilitado; no puede modificar su valor." @@ -20610,8 +20240,18 @@ msgid "Successful Proctored Exam" msgstr "Examen supervisado exitoso" #: cms/templates/settings.html -msgid "Successful In Course Reverification" -msgstr "Reverificación exitosa durante el curso" +msgid "Proctored Exam {number}" +msgstr "Examen supervisado {number}" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "Re verificación dentro del curso exitosa" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" +msgstr "Re verificación dentro del curso {number}" #: cms/templates/settings.html msgid "Access to {display_name}" @@ -21456,6 +21096,10 @@ msgstr "Como usar {studio_name} para construir su curso" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "¿Tiene problemas, preguntas o sugerencias acerca de {studio_name}?" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "nombre" + # empty msgid "This is a key string." msgstr "Esta es una cadena de texto." diff --git a/conf/locale/es_419/LC_MESSAGES/djangojs.mo b/conf/locale/es_419/LC_MESSAGES/djangojs.mo index ec8e874720..4ad422820d 100644 Binary files a/conf/locale/es_419/LC_MESSAGES/djangojs.mo and b/conf/locale/es_419/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/es_419/LC_MESSAGES/djangojs.po b/conf/locale/es_419/LC_MESSAGES/djangojs.po index a77a23163c..028536b65e 100644 --- a/conf/locale/es_419/LC_MESSAGES/djangojs.po +++ b/conf/locale/es_419/LC_MESSAGES/djangojs.po @@ -29,6 +29,7 @@ # Nuri Plans Toral , 2015 # Cristian Salamea , 2013 # paul ochoa , 2015 +# Sarina Canelake , 2015 # Valeria Freire , 2014-2015 # #-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-# # edX translation file. @@ -88,9 +89,9 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" -"Last-Translator: Sarina Canelake \n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-29 00:56+0000\n" +"Last-Translator: Juan Camilo Montoya Franco \n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/projects/p/edx-platform/language/es_419/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -151,11 +152,6 @@ msgstr "Aceptar" msgid "Cancel" msgstr "Cancelar" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "Este vínculo se abrirá en una nueva ventana o pestaña del navegador" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -246,10 +242,12 @@ msgstr "Error" msgid "Save" msgstr "Guardar" +#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: cms/static/js/views/modals/edit_xblock.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: cms/templates/js/signatory-editor.underscore msgid "Close" msgstr "Cerrar" @@ -295,6 +293,12 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "(%(num_points)s punto posible)" msgstr[1] "(%(num_points)s puntos posibles)" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" +"El proceso de calificación todavía está corriendo. Recargue la página para " +"ver las actualizaciones." + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "Respuesta:" @@ -2159,6 +2163,8 @@ msgid "" "Showing %(current_item_range)s out of %(total_items_count)s, filtered by " "%(asset_type)s, sorted by %(sort_name)s ascending" msgstr "" +"Mostrando %(current_item_range)s de %(total_items_count)s, filtrados por " +"%(asset_type)s, ordenados por %(sort_name)s ascendentemente" #. Translators: sample result: #. "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added @@ -2168,6 +2174,8 @@ msgid "" "Showing %(current_item_range)s out of %(total_items_count)s, filtered by " "%(asset_type)s, sorted by %(sort_name)s descending" msgstr "" +"Mostrando %(current_item_range)s de %(total_items_count)s, filtrados por " +"%(asset_type)s, ordenados por %(sort_name)s descendentemente" #. Translators: sample result: #. "Showing 0-9 out of 25 total, sorted by Date Added ascending" @@ -2176,6 +2184,8 @@ msgid "" "Showing %(current_item_range)s out of %(total_items_count)s, sorted by " "%(sort_name)s ascending" msgstr "" +"Mostrando %(current_item_range)s de %(total_items_count)s, ordenados por " +"%(sort_name)s ascendentemente" #. Translators: sample result: #. "Showing 0-9 out of 25 total, sorted by Date Added descending" @@ -2184,12 +2194,14 @@ msgid "" "Showing %(current_item_range)s out of %(total_items_count)s, sorted by " "%(sort_name)s descending" msgstr "" +"Mostrando %(current_item_range)s de %(total_items_count)s, ordenados por " +"%(sort_name)s descendentemente" #. Translators: turns into "25 total" to be used in other sentences, e.g. #. "Showing 0-9 out of 25 total". #: common/static/common/js/components/views/paging_header.js msgid "%(total_items)s total" -msgstr "" +msgstr "%(total_items)s total" #: common/static/js/capa/drag_and_drop/base_image.js msgid "Drop target image" @@ -2411,13 +2423,8 @@ msgstr "Responder" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/templates/edxnotes/note-item.underscore -#, fuzzy msgid "Tags:" -msgstr "" -"#-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#\n" -"Etiquetas\n" -"#-#-#-#-# underscore.po (edx-platform) #-#-#-#-#\n" -"Etiquetas:" +msgstr "Etiquetas:" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" @@ -2443,8 +2450,8 @@ msgstr "Explorar" #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "%(team_count)s Team" msgid_plural "%(team_count)s Teams" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(team_count)s Equipo" +msgstr[1] "%(team_count)s Equipos" #: lms/static/coffee/src/calculator.js msgid "Open Calculator" @@ -3340,6 +3347,7 @@ msgstr "Introduzca fecha de vencimiento" msgid "Enter Start Date" msgstr "Introduzca fecha de inicio" +#: lms/static/js/courseware/credit_progress.js #: lms/templates/edxnotes/note-item.underscore msgid "Less" msgstr "Menos" @@ -3424,7 +3432,7 @@ msgstr "" #. translated. #: lms/static/js/edxnotes/views/tabs/tags.js msgid "[no tags]" -msgstr "[Sin etiquetas]" +msgstr "[sin etiquetas]" #: lms/static/js/groups/views/cohort_discussions.js #: lms/static/js/groups/views/cohort_discussions_course_wide.js @@ -3567,6 +3575,8 @@ msgid "" "There was a problem creating the report. Select \"Create Executive Summary\"" " to try again." msgstr "" +"Hubo un problema creando el reporte. Seleccione \"Crear resumen ejecutivo\" " +"para intentarlo nuevamente." #: lms/static/js/instructor_dashboard/ecommerce.js msgid "Enter the enrollment code." @@ -3646,19 +3656,19 @@ msgstr "Por favor revise su correo para confirmar el cambio" #: lms/static/js/student_account/views/FinishAuthView.js msgid "Saving your email preference" -msgstr "" +msgstr "Guardando sus preferencias de correo electrónico" #: lms/static/js/student_account/views/FinishAuthView.js msgid "Enrolling you in the selected course" -msgstr "" +msgstr "Inscribiéndolo en el curso seleccionado" #: lms/static/js/student_account/views/FinishAuthView.js msgid "Adding the selected course to your cart" -msgstr "" +msgstr "Añadiendo el curso seleccionado a su carrito de compras" #: lms/static/js/student_account/views/FinishAuthView.js msgid "Loading your courses" -msgstr "" +msgstr "Cargando sus cursos" #: lms/static/js/student_account/views/account_settings_factory.js msgid "Basic Account Information (required)" @@ -3945,6 +3955,19 @@ msgstr "No se pudo publicar su petición" msgid "Could not retrieve payment information" msgstr "No se pudo conseguir la información del pago" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "Tome una foto de su ID" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "Revise su información" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "Confirmar" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "Ocurrió un error. Por favor, intente recargar la página." @@ -4138,6 +4161,10 @@ msgstr "Borrando" msgid "OpenAssessment Save Error" msgstr "Error al guardar en el servidor OpenAssessment" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "Este vínculo se abrirá en una nueva ventana o pestaña del navegador" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "Este vínculo se abrirá en una ventana emergente" @@ -4424,6 +4451,9 @@ msgstr "" " un archivo que termine en <%= fileExtensions %> para ser cargado." #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "o" @@ -5621,12 +5651,55 @@ msgstr "Ocurrió un error. Por favor recargue la página." msgid "Forgot password?" msgstr "¿Olvidaste tu contraseña?" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "Iniciar sesión" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "¿Desea iniciar sesión usando %(providerName)s?" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "Iniciar sesión usando %(providerName)s" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "Mostrar otras formas de iniciar sesión o registrarme" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "Iniciar sesión con las credenciales de la institución o el Campus" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "Elija su institución:" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "Volver al inicio" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "Registrarse con las credenciales de la institución o el Campus" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "Registrarse a través de edX" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " "%(currentProvider)s account does not have a linked %(platformName)s account." " To link your accounts, sign in now using your %(platformName)s password." msgstr "" +"Ha iniciado sesión exitosamente en %(currentProvider)s, pero su cuenta de " +"%(currentProvider)s no está vinculada con una cuenta en %(platformName)s. " +"Para vincular sus cuentas, ingrese con su usuario y contraseña de " +"%(platformName)s." #: lms/templates/student_account/login.underscore msgid "Password Reset Email Sent" @@ -5646,17 +5719,17 @@ msgstr "No se ha podido iniciar su sesión." #: lms/templates/student_account/login.underscore msgid "An error occurred when signing you in to %(platformName)s." -msgstr "" - -#: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "Iniciar sesión" +msgstr "Ocurrió un error al iniciar su sesión en %(platformName)s." #: lms/templates/student_account/login.underscore msgid "or sign in with" msgstr "o inicie sesión con" +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" +msgstr "Usar mis credenciales de la institución o el Campus" + #: lms/templates/student_account/login.underscore msgid "New to %(platformName)s?" msgstr "¿Es nuevo en %(platformName)s?" @@ -6121,6 +6194,26 @@ msgstr "" "Una licencia de conducir, pasaporte, cédula o otra identificación oficial " "con su nombre y foto" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "Verificación de identidad en progreso" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" +"Hemos recibido la información enviada y estamos verificando su identidad. " +"Recibir un mensaje en su Panel principal cuando el proceso de verificación " +"esté completado (usualmente entre 1-2 días). Durante este tiempo, igualmente" +" tendrá acceso a todo el contenido de su curso." + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "Volver al panel principal" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "Revisar sus fotos" @@ -6186,10 +6279,6 @@ msgstr "Tome nuevamente sus fotos" msgid "Before proceeding, please confirm that your details match" msgstr "Antes de continuar, por favor confirme que sus datos sean correctos." -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "Confirmar" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " @@ -6224,7 +6313,7 @@ msgstr "Tipos de problemas comunes" #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problems with Hints and Feedback" -msgstr "" +msgstr "Problemas comunes con las pistas y los comentarios" #: cms/templates/js/add-xblock-component.underscore msgid "Add New Component" diff --git a/conf/locale/fr/LC_MESSAGES/django.mo b/conf/locale/fr/LC_MESSAGES/django.mo index c559574c6e..ede5188c8f 100644 Binary files a/conf/locale/fr/LC_MESSAGES/django.mo and b/conf/locale/fr/LC_MESSAGES/django.mo differ diff --git a/conf/locale/fr/LC_MESSAGES/django.po b/conf/locale/fr/LC_MESSAGES/django.po index 5ab3eab889..f3503e5140 100644 --- a/conf/locale/fr/LC_MESSAGES/django.po +++ b/conf/locale/fr/LC_MESSAGES/django.po @@ -52,7 +52,7 @@ # Toreador , 2014 # Xavier Antoviaque , 2014 # PETIT Yannick , 2013 -# yepelboin , 2014 +# yepelboin , 2014-2015 # #-#-#-#-# django-studio.po (edx-platform) #-#-#-#-# # edX translation file. # Copyright (C) 2015 EdX @@ -70,6 +70,7 @@ # Julien Rolland , 2013 # Jeremie Sicsic , 2013 # Julien Colmonts , 2014 +# moocit-france , 2015 # Nikolaos Maris, 2013 # Olivier Marquez , 2013 # Pierre-Emmanuel Colas , 2015 @@ -79,6 +80,7 @@ # Steven BERNARD , 2013 # PETIT Yannick , 2013 # ytabaa , 2014 +# yepelboin , 2015 # #-#-#-#-# mako.po (edx-platform) #-#-#-#-# # edX translation file # Copyright (C) 2015 edX @@ -123,6 +125,7 @@ # Steven BERNARD , 2014 # Xavier Antoviaque , 2014 # PETIT Yannick , 2013 +# yepelboin , 2015 # #-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-# # edX translation file # Copyright (C) 2015 edX @@ -173,8 +176,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" -"PO-Revision-Date: 2015-05-28 20:00+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" +"PO-Revision-Date: 2015-06-19 17:16+0000\n" "Last-Translator: Xavier Antoviaque \n" "Language-Team: French (http://www.transifex.com/projects/p/edx-platform/language/fr/)\n" "MIME-Version: 1.0\n" @@ -253,13 +256,8 @@ msgstr "Vidéo ID" #: lms/djangoapps/shoppingcart/reports.py #: lms/templates/open_ended_problems/open_ended_problems.html #: lms/templates/shoppingcart/receipt.html -#, fuzzy msgid "Status" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Statut\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"État" +msgstr "Statut" #: common/djangoapps/config_models/admin.py msgid "Revert to the selected configuration" @@ -341,13 +339,8 @@ msgstr "" #: common/djangoapps/course_modes/models.py lms/djangoapps/branding/api.py #: openedx/core/djangoapps/user_api/views.py #: lms/templates/static_templates/honor.html -#, fuzzy msgid "Honor Code" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Charte utilisateur\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Code d'honneur" +msgstr "Code d'honneur" #: common/djangoapps/course_modes/models.py msgid "You're auditing this course" @@ -514,7 +507,7 @@ msgstr "Un pays est requis" #: common/djangoapps/student/forms.py msgid "To enroll, you must follow the honor code." -msgstr "Pour vous inscrire, vous devez adopter la charte utilisateur." +msgstr "Pour vous inscrire, vous devez accepter le code d'honneur." #: common/djangoapps/student/forms.py msgid "You are missing one or more required fields" @@ -637,7 +630,7 @@ msgstr "" #: common/djangoapps/student/views.py msgid "Course id not specified" -msgstr "Id de Cours non spécifié" +msgstr "Id du Cours non spécifié" #: common/djangoapps/student/views.py lms/djangoapps/dashboard/support.py msgid "Invalid course id" @@ -645,7 +638,7 @@ msgstr "Identifiant de cours invalide" #: common/djangoapps/student/views.py msgid "Course id is invalid" -msgstr "Id de cours invalide" +msgstr "Id du cours invalide" #: common/djangoapps/student/views.py msgid "Could not enroll" @@ -716,13 +709,8 @@ msgid "Too many failed login attempts. Try again later." msgstr "Trop de tentatives de connexion échouées. Réessayez plus tard." #: common/djangoapps/student/views.py lms/templates/provider_login.html -#, fuzzy msgid "Email or password is incorrect." -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"L'adresse email ou le mot de passe est incorrect\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Adresse mail ou mot de passe incorrect." +msgstr "Adresse mail ou mot de passe incorrect." #: common/djangoapps/student/views.py msgid "" @@ -845,13 +833,28 @@ msgstr "" "Impossible d'envoyer l'email contenant le lien d'activation. Veuillez " "réessayer plus tard." -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "Nom requis" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "ID invalide" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1299,6 +1302,24 @@ msgstr "sans réponse" msgid "processing" msgstr "en cours" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -2082,13 +2103,8 @@ msgstr "Quelle étape au sein de la tâche courante de l'étudiant." #: common/lib/xmodule/xmodule/combined_open_ended_module.py #: common/lib/xmodule/xmodule/peer_grading_module.py #: lms/templates/peer_grading/peer_grading.html -#, fuzzy msgid "Graded" msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Evalué\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Noté" #: common/lib/xmodule/xmodule/combined_open_ended_module.py msgid "" @@ -2109,7 +2125,7 @@ msgstr "" #: common/lib/xmodule/xmodule/combined_open_ended_module.py msgid "Allow File Uploads" -msgstr "Autoriser l'envoi de fichiers" +msgstr "Autoriser le chargement de fichiers" #: common/lib/xmodule/xmodule/combined_open_ended_module.py msgid "Whether or not the student can submit files as a response." @@ -2210,6 +2226,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "A déterminer" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "Passeports LTI" @@ -3155,13 +3181,6 @@ msgstr "" msgid "General" msgstr "Général" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "A déterminer" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "Id de Discussion" @@ -5112,13 +5131,8 @@ msgstr "Pourcentage des étudiants" #: lms/djangoapps/class_dashboard/dashboard_data.py #: lms/templates/folditchallenge.html -#, fuzzy msgid "Score" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Score\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Note" +msgstr "Note" #: lms/djangoapps/class_dashboard/dashboard_data.py msgid "problems" @@ -5357,13 +5371,8 @@ msgstr "Adresse e-mail" #: lms/templates/sysadmin_dashboard_gitlogs.html #: lms/templates/instructor/instructor_dashboard_2/add_coupon_modal.html #: lms/templates/instructor/instructor_dashboard_2/edit_coupon_modal.html -#, fuzzy msgid "Course ID" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"ID du cours\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"ID de Cours" +msgstr "ID de Cours" #: lms/djangoapps/dashboard/support.py msgid "User not found" @@ -5562,13 +5571,8 @@ msgstr "Le cours {course_name} a été chargé
      Erreurs:" #: lms/djangoapps/dashboard/sysadmin.py cms/templates/course-create-rerun.html #: cms/templates/index.html lms/templates/shoppingcart/receipt.html -#, fuzzy msgid "Course Name" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Nom du cours\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Nom du Cours" +msgstr "Nom du cours" #: lms/djangoapps/dashboard/sysadmin.py msgid "Directory/ID" @@ -5838,13 +5842,8 @@ msgstr "ID de l'utilisateur" #: openedx/core/djangoapps/user_api/views.py #: lms/templates/courseware/legacy_instructor_dashboard.html #: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html -#, fuzzy msgid "Email" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"E-mail\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Email" +msgstr "E-mail" #: lms/djangoapps/instructor/views/api.py #: lms/djangoapps/instructor_task/tasks_helper.py lms/envs/devstack.py @@ -5872,13 +5871,8 @@ msgstr "Genre" #: lms/djangoapps/instructor/views/api.py #: lms/djangoapps/instructor_task/tasks_helper.py #: lms/templates/instructor/instructor_dashboard_2/instructor_analytics.html -#, fuzzy msgid "Level of Education" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Niveau de formation\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Niveau de Formation" +msgstr "Niveau de formation" #: lms/djangoapps/instructor/views/api.py #: lms/djangoapps/instructor_task/tasks_helper.py lms/templates/register.html @@ -6209,13 +6203,8 @@ msgstr "Extensions" #: lms/djangoapps/instructor/views/instructor_dashboard.py #: lms/templates/instructor/instructor_dashboard_2/data_download.html -#, fuzzy msgid "Data Download" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Téléchargement Données\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Téléchargement des données" +msgstr "Téléchargement des données" #: lms/djangoapps/instructor/views/instructor_dashboard.py #: lms/templates/courseware/legacy_instructor_dashboard.html @@ -6348,7 +6337,6 @@ msgstr "Identifiant" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "Nom complet" @@ -6455,7 +6443,7 @@ msgstr "envoyé par e-mail" #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py msgid "graded" -msgstr "Noté" +msgstr "noté" #. Translators: This is a past-tense phrase that is inserted into task #. progress messages as {action}. @@ -6887,13 +6875,8 @@ msgstr "Les soumissions ont été signalées pour une révision." #: lms/djangoapps/open_ended_grading/views.py #: lms/templates/instructor/staff_grading.html -#, fuzzy msgid "Staff grading" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Note de l'équipe pédagogique\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Notation de l'équipe pédagogique" +msgstr "Notation de l'équipe pédagogique" #. Translators: "Peer grading" appears on a tab that allows #. students to view open-ended problems that require grading @@ -7228,7 +7211,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "Vous devez être connecté pour ajouter à un panier" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "Le cours demandé n'existe pas." @@ -7916,19 +7898,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "Télécharger les rapports CSV" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"Il y a une erreur dans le format de votre date. Elle doit être sous la forme" -" AAAA-MM-JJ" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -8036,9 +8005,9 @@ msgstr "" msgid "Payment confirmation" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/djangoapps/verify_student/views.py msgid "Take photo" -msgstr "Prendre une photo" +msgstr "" #: lms/djangoapps/verify_student/views.py msgid "Take a photo of your ID" @@ -8777,7 +8746,7 @@ msgstr "Pièces jointes" #: lms/templates/wiki/plugins/attachments/index.html msgid "Upload new file" -msgstr "Déposer un nouveau fichier" +msgstr "Charger un nouveau fichier" #: lms/templates/wiki/plugins/attachments/index.html msgid "Search and add file" @@ -8785,11 +8754,11 @@ msgstr "Chercher et ajouter un fichier" #: lms/templates/wiki/plugins/attachments/index.html msgid "Upload File" -msgstr "Uploader un fichier" +msgstr "Charger un fichier" #: lms/templates/wiki/plugins/attachments/index.html msgid "Upload file" -msgstr "Déposer un fichier" +msgstr "Charger un fichier" #: lms/templates/wiki/plugins/attachments/index.html msgid "Search files and articles" @@ -8819,7 +8788,7 @@ msgstr "Élément surligné" #: lms/templates/wiki/plugins/attachments/index.html msgid "Uploaded by" -msgstr "Uploadé par" +msgstr "Chargé par" #: lms/templates/wiki/plugins/attachments/index.html msgid "Size" @@ -9180,7 +9149,7 @@ msgstr "" #: cms/djangoapps/contentstore/views/assets.py msgid "Upload completed" -msgstr "Téléchargement terminé" +msgstr "Chargement terminé" #: cms/djangoapps/contentstore/views/certificates.py msgid "must have name of the certificate" @@ -9249,6 +9218,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -9283,7 +9260,7 @@ msgstr "Nous acceptons seulement l'upload d'un fichier .tar.gz." #: cms/djangoapps/contentstore/views/import_export.py msgid "File upload corrupted. Please try again" -msgstr "Fichier Uploadé corrompu. Merci d'essayer à nouveau." +msgstr "Fichier envoyé corrompu. Merci d'essayer à nouveau." #: cms/djangoapps/contentstore/views/import_export.py msgid "Could not find the {0} file in the package." @@ -9386,7 +9363,7 @@ msgstr "" #: cms/templates/import.html msgid "Uploading" -msgstr "Upload en cours" +msgstr "Chargement en cours" #. Translators: This is the status for a video that the servers are currently #. processing @@ -9514,7 +9491,6 @@ msgid "Loading" msgstr "Chargement en cours" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "fermer" @@ -9565,7 +9541,7 @@ msgstr "Cours" #: lms/templates/provider_login.html lms/templates/register-shib.html #: lms/templates/register.html lms/templates/signup_modal.html msgid "E-mail" -msgstr "Email" +msgstr "E-mail" #. Translators: This is the placeholder text for a field that requests an #. email @@ -9655,11 +9631,6 @@ msgstr "Aide" msgid "Sign Out" msgstr "Se déconnecter" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -9688,10 +9659,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "Mettez à jour votre inscription pour {} | Choisissez votre parcours" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "Inscrit dans {} | Choisissez votre parcours" @@ -9700,6 +9667,10 @@ msgstr "Inscrit dans {} | Choisissez votre parcours" msgid "Sorry, there was an error when trying to enroll you" msgstr "Désolé, une erreur s'est produite en tentant de vous inscrire." +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -9775,9 +9746,9 @@ msgid "" "Certificate to showcase your achievement." msgstr "" "Suivez gratuitement ce cours et accédez à toutes les données, activités, " -"tests et forum. Si votre travail est satisfaisant et que vous suivez le Code" -" d'Honneur, vous recevrez un Certificat de Code d'Honneur personnalisé pour " -"officialiser votre réussite." +"tests et forum. Si votre travail est satisfaisant et si vous adhérez à la " +"charte utilisateur, vous recevrez un Certificat sur l'honneur personnalisé " +"pour officialiser votre réussite." #: lms/templates/admin_dashboard.html msgid "{platform_name}-wide Summary" @@ -9878,7 +9849,7 @@ msgstr "Voir l'unité dans Studio" #: lms/templates/email_change_failed.html lms/templates/email_exists.html msgid "E-mail change failed" -msgstr "Changement d'adresse email échoué" +msgstr "Changement d'adresse e-mail échoué" #: lms/templates/email_change_failed.html msgid "We were unable to send a confirmation email to {email}" @@ -9892,18 +9863,18 @@ msgstr "Retour à {link_start}l'accueil{link_end}" #: lms/templates/email_change_successful.html #: lms/templates/emails_change_successful.html msgid "E-mail change successful!" -msgstr "Adresse email modifiée avec succès !" +msgstr "Adresse e-mail modifiée avec succès !" #: lms/templates/email_change_successful.html #: lms/templates/emails_change_successful.html msgid "You should see your new email in your {link_start}dashboard{link_end}." msgstr "" -"Vous devez voir votre nouvelle adresse email sur votre {link_start}Tableau " +"Vous devez voir votre nouvelle adresse e-mail sur votre {link_start}Tableau " "de bord{link_end}." #: lms/templates/email_exists.html msgid "An account with the new e-mail address already exists." -msgstr "Un compte avec la nouvelle adresse email existe déjà." +msgstr "Un compte avec la nouvelle adresse e-mail existe déjà." #: lms/templates/enroll_students.html msgid "Student Enrollment Form" @@ -9996,17 +9967,17 @@ msgid "" "Please enter your e-mail address below, and we will e-mail instructions for " "setting a new password." msgstr "" -"Merci d'entrer votre adresse email ci-dessous, nous vous enverrons des " +"Merci d'entrer votre adresse e-mail ci-dessous, nous vous enverrons des " "instructions pour choisir un nouveau mot de passe" #: lms/templates/forgot_password_modal.html msgid "Your E-mail Address" -msgstr "Votre adresse email" +msgstr "Votre adresse e-mail" #: lms/templates/forgot_password_modal.html lms/templates/login.html msgid "This is the e-mail address you used to register with {platform}" msgstr "" -"Voici l'adresse email que vous avez utilisée pour vous inscrire sur " +"Voici l'adresse e-mail que vous avez utilisée pour vous inscrire sur " "{platform}" #: lms/templates/forgot_password_modal.html @@ -10015,7 +9986,7 @@ msgstr "Réinitialiser mon mot de passe" #: lms/templates/forgot_password_modal.html msgid "Email is incorrect." -msgstr "L’adresse email est incorrecte." +msgstr "L’adresse e-mail est incorrecte." #: lms/templates/help_modal.html msgid "{platform_name} Help" @@ -10163,7 +10134,7 @@ msgstr "Une erreur s'est produite." #: lms/templates/help_modal.html msgid "Please {link_start}send us e-mail{link_end}." -msgstr "Veuillez {link_start}nous envoyer un email{link_end}." +msgstr "Veuillez {link_start}nous envoyer un e-mail{link_end}." #: lms/templates/help_modal.html msgid "Please try again later." @@ -10209,7 +10180,7 @@ msgstr "Clé de changement de mail invalide" #: lms/templates/invalid_email_key.html msgid "This e-mail key is not valid. Please check:" -msgstr "Cette clé email n'est pas valide. Merci de vérifier :" +msgstr "Cette clé e-mail n'est pas valide. Merci de vérifier :" #: lms/templates/invalid_email_key.html msgid "" @@ -10217,7 +10188,7 @@ msgid "" "happened." msgstr "" "Est ce que cette clé a déjà été utilisée ? Verifiez si le changement " -"d'adresse email a déjà eu lieu." +"d'adresse e-mail a déjà eu lieu." #: lms/templates/invalid_email_key.html msgid "Did your e-mail client break the URL into two lines?" @@ -10312,7 +10283,7 @@ msgstr "Nous n'avons pas pu vous connecter." #: lms/templates/login.html msgid "Your email or password is incorrect" -msgstr "Votre adresse email ou mot de passe est incorrect" +msgstr "Votre adresse e-mail ou mot de passe est incorrect" #: lms/templates/login.html msgid "An error occurred when signing you in to {platform_name}." @@ -10413,7 +10384,7 @@ msgstr "Réactiver le compte" #: lms/templates/manage_user_standing.html msgid "Remove Profile Image" -msgstr "Enlever l'image de profil" +msgstr "Supprimer l'image de profil" #: lms/templates/manage_user_standing.html msgid "Students whose accounts have been disabled" @@ -10665,7 +10636,7 @@ msgstr "" "d'activation pour compléter l'inscription. Vous ne voyez pas le mail ? " "Vérifiez votre dossier de spam et marquez les mails provenant de " "class.stanford.edu comme n'étant pas du spam, puisque vous voudrez pouvoir " -"recevoir des emails provenant de nos cours." +"recevoir des e-mails provenant de nos cours." #: lms/templates/register-sidebar.html msgid "" @@ -10984,7 +10955,7 @@ msgstr "Gestion des utilisateurs" #: lms/templates/sysadmin_dashboard.html msgid "Email or username" -msgstr "Email ou nom d'utilisateur" +msgstr "E-mail ou nom d'utilisateur" #: lms/templates/sysadmin_dashboard.html msgid "Delete user" @@ -11669,12 +11640,11 @@ msgid "" "automatically, and will be returned within 30 minutes if the original is " "already graded, or when the original is graded if not." msgstr "" -"Attention à l'équipe pédagogique : veuillez noter que si vous soumettez un " -"duplicata d'une texte qui a déjà été soumis à la notation, il n'apparaîtra " -"pas dans la page de notation de l'équipe pédagogique. Il se fera attribuer " -"la même note que celle attribuée automatiquement à l'original , et sera " -"retourné dans les 30 minutes si l'original a déjà été noté ou quand " -"l'original le sera dans le cas contraire." +"Remarque pour l'équipe pédagogique : veuillez noter que si vous soumettez un" +" duplicata d'un texte, qui a déjà été soumis à la notation, il n'apparaîtra " +"pas dans la page de notation de l'équipe pédagogique. La note sera celle " +"attribuée automatiquement à l'original , et sera retourné dans les 30 " +"minutes si l'original a déjà été noté ou plus tard dans le cas contraire." #: lms/templates/combinedopenended/combined_open_ended_legend.html msgid "Legend" @@ -11902,7 +11872,7 @@ msgstr "Tweetez que vous êtes inscrits pour ce cours" #: lms/templates/courseware/course_about.html msgid "Email someone to say you've registered for this course" -msgstr "Envoyez par email que vous êtes inscrits à ce cours" +msgstr "Envoyez par e-mail que vous êtes inscrits à ce cours" #. Translators: This text will be automatically posted to the student's #. Twitter account. {url} should appear at the end of the text. @@ -12304,7 +12274,7 @@ msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "To send email, visit the Email section of the Instructor Dashboard." msgstr "" -"Pour envoyer un email, visitez la section Email de l'onglet Instructeur." +"Pour envoyer un e-mail, visitez la section E-mail de l'onglet Instructeur." #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "No Analytics are available at this time." @@ -12568,16 +12538,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -12593,9 +12567,8 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" -msgstr "Plus" +msgid "Less" +msgstr "" #: lms/templates/courseware/progress.html msgid "{earned:.3n} of {total:.3n} possible points" @@ -12896,6 +12869,37 @@ msgstr "" "compte{contact_link_end} pour demander le paiement, ou vous pouvez vous " "{unenroll_link_start}désinscrire{unenroll_link_end} de ce cours" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "éditer" @@ -13251,8 +13255,8 @@ msgid "" "Check this box to receive an email digest once a day notifying you about " "new, unread activity from posts you are following." msgstr "" -"Cocher cette case pour recevoir une fois par jour un email récapitulatif des" -" nouveautés dans les fils de discussion que vous suivez." +"Cocher cette case pour recevoir une fois par jour un e-mail récapitulatif " +"des nouveautés dans les fils de discussion que vous suivez." #. Translators: This labels the selector for which group of students can view #. a @@ -13387,6 +13391,10 @@ msgstr "Ne plus épingler" msgid "Open" msgstr "Ouvrir" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "Plus" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -13522,7 +13530,7 @@ msgstr "" #: lms/templates/emails/account_creation_and_enroll_emailMessage.txt msgid "email: {email}" -msgstr "email: {email}" +msgstr "e-mail: {email}" #: lms/templates/emails/account_creation_and_enroll_emailMessage.txt msgid "password: {password}" @@ -13596,7 +13604,7 @@ msgid "" msgstr "" "Si vous n'avez pas demandé ceci, vous n'avez pas besoin de faire quoi que ce" " soit; vous ne recevrez plus de courriers électroniques de notre part. Merci" -" de ne pas répondre à cet email; si vous avez besoin d'obtenir une " +" de ne pas répondre à cet e-mail; si vous avez besoin d'obtenir une " "assistance, consultez la section d'aide du site web {platform_name}." #: lms/templates/emails/activation_email_subject.txt @@ -13641,7 +13649,7 @@ msgstr "" #: lms/templates/emails/unenroll_email_allowedmessage.txt msgid "This email was automatically sent from {site_name} to {email_address}" msgstr "" -"Cet email a été envoyé automatiquement de {site_name} à {email_address}" +"Cet e-mail a été envoyé automatiquement de {site_name} à {email_address}" #: lms/templates/emails/add_beta_tester_email_subject.txt msgid "You have been invited to a beta test for {course_name}" @@ -13718,7 +13726,7 @@ msgstr "Nom du Contact de l'Entreprise:" #: lms/templates/emails/business_order_confirmation_email.txt msgid "Company Contact Email:" -msgstr "Email du Contact de l'Entreprise:" +msgstr "E-mail du Contact de l'Entreprise:" #. Translators: this will be the name of a person receiving an email #: lms/templates/emails/business_order_confirmation_email.txt @@ -13728,7 +13736,7 @@ msgstr "Nom du Bénéficiaire:" #. Translators: this will be the email address of a person receiving an email #: lms/templates/emails/business_order_confirmation_email.txt msgid "Recipient Email:" -msgstr "Email du Bénéficiaire:" +msgstr "E-mail du Bénéficiaire:" #: lms/templates/emails/business_order_confirmation_email.txt #: lms/templates/emails/order_confirmation_email.txt @@ -13747,7 +13755,7 @@ msgid "" msgstr "" "Un fichier CSV contenant vos URL d'inscription est joint. Veuillez " "distribuer ces URL à chaque étudiant souhaitant s'inscrire en utilisant le " -"modèle d'email ci-dessous." +"modèle d'e-mail ci-dessous." #. Translators: This is followed by the instructor or course team name (so #. could be singular or plural) @@ -13874,7 +13882,7 @@ msgstr "" #: lms/templates/emails/email_change_subject.txt msgid "Request to change {platform_name} account e-mail" msgstr "" -"Demande de modification de l'adresse email du compte sur {platform_name}" +"Demande de modification de l'adresse e-mail du compte sur {platform_name}" #: lms/templates/emails/enroll_email_allowedmessage.txt msgid "Dear student," @@ -13908,7 +13916,7 @@ msgid "" msgstr "" "Pour terminer votre inscription, rendez-vous sur {registration_url} et " "complétez le formulaire d'inscription. Soyez vigilant d'utiliser " -"{email_address} dans le champ Email. " +"{email_address} dans le champ E-mail. " #: lms/templates/emails/enroll_email_allowedmessage.txt msgid "" @@ -13983,7 +13991,7 @@ msgid "" "can check the status of the verification process on your dashboard." msgstr "" "Nous avons reçu vos informations et la vérification est en cours. Vous " -"pouvez suivre le statut de la vérification sur votre tableau de bord." +"pouvez suivre l'état de la vérification sur votre tableau de bord." #: lms/templates/emails/photo_submission_confirmation.txt #: lms/templates/emails/reverification_processed.txt @@ -14270,7 +14278,7 @@ msgstr "Collectes de Dons" #: lms/templates/instructor/staff_grading.html msgid "{course_number} Staff Grading" -msgstr "Notation de l'équipe pédagogique {course_number}" +msgstr "Notes de l'équipe pédagogique {course_number}" #: lms/templates/instructor/staff_grading.html msgid "" @@ -14282,14 +14290,13 @@ msgid "" " grading, though with diminishing returns. You can see the current accuracy " "of AI grading in the problem view." msgstr "" -"Voici la liste des problèmes qui doivent actuellement être notés afin " -"d’entraîner le système AI de notation et de créer des tests de calibration " -"pour la notation par les pairs. Chaque problème doit être traité séparément," -" et nous avons indiqué le nombre de soumissions d'étudiants qui doivent être" -" notées. Vous pouvez évaluer plus de soumissions que le nombre minimal " -"requis - cela permettra d'améliorer la précision de l'AI, mais avec des " -"rendements décroissants. Vous pouvez voir la précision actuelle de l'IA dans" -" la vue du problème." +"Liste des problèmes qui doivent être notés afin d’entraîner le système IA " +"de notation et de créer des tests de calibration pour l'évaluation par les " +"pairs. Chaque problème doit être traité séparément, et nous avons indiqué le" +" nombre de soumissions d'étudiants qui doivent être notées. Vous pouvez " +"évaluer plus de soumissions que le nombre minimal requis - cela permettra " +"d'améliorer la précision de l'IA, mais avec des rendements décroissants. " +"Vous pouvez voir la précision actuelle de l'IA dans la vue du problème." #: lms/templates/instructor/staff_grading.html msgid "Problem List" @@ -14306,8 +14313,8 @@ msgstr "" "Veuillez noter que si vous voyez une soumission ici, c'est qu'elle a " "temporairement été retirée de la liste à évaluer. Cette soumission " "retournera dans la liste à évaluer après 30 minutes sans notation soumise. " -"Cliquer sur le bouton Retour donnera une attente de 30 minutes pour pouvoir " -"noter à nouveau cette soumission." +"Cliquer sur le bouton Retour résultera en une attente de 30 minutes pour " +"pouvoir noter à nouveau cette soumission." #: lms/templates/instructor/staff_grading.html msgid "Prompt" @@ -14467,7 +14474,7 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html #: lms/templates/instructor/instructor_dashboard_2/student_admin.html msgid "The status for any active tasks appears in a table below." -msgstr "Le statut des tâches actives apparaît dans la table ci-dessous." +msgstr "L'état des tâches actives apparaît dans la table ci-dessous." #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Course Warnings" @@ -14999,13 +15006,13 @@ msgstr "" msgid "" "Specify the {platform_name} email address or username of a student here:" msgstr "" -"Spécifiez l'adresse email {platform_name} ou le nom d'utilisateur d'un " +"Spécifiez l'adresse e-mail {platform_name} ou le nom d'utilisateur d'un " "étudiant ici :" #: lms/templates/instructor/instructor_dashboard_2/extensions.html #: lms/templates/instructor/instructor_dashboard_2/student_admin.html msgid "Student Email or Username" -msgstr "Adresse email Étudiant ou Nom d'utilisateur" +msgstr "Adresse e-mail Étudiant ou Nom d'utilisateur" #: lms/templates/instructor/instructor_dashboard_2/extensions.html msgid "Choose the graded unit:" @@ -15260,7 +15267,7 @@ msgid "" msgstr "" "Pour enregistrer et inscrire une liste d'utilisateurs dans ce cours, " "choisissez un fichier csv qui contient les colonnes suivantes dans le même " -"ordre: email, nom d'utilisateur, nom, et pays. Ne saisissez qu'un seul " +"ordre: e-mail, nom d'utilisateur, nom, et pays. Ne saisissez qu'un seul " "utilisateur par ligne et ne mettez pas d'en-têtes, pied de page ou de lignes" " vides. " @@ -15533,7 +15540,7 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Send Email" -msgstr "Envoyer l'email" +msgstr "Envoyer l'e-mail" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Send to:" @@ -15568,7 +15575,7 @@ msgid "" "Please try not to email students more than once per week. Before sending " "your email, consider:" msgstr "" -"Veuillez éviter d'envoyer plus d'un email par semaine aux étudiants. Avant " +"Veuillez éviter d'envoyer plus d'un e-mail par semaine aux étudiants. Avant " "un envoi, merci de prendre en considération :" #: lms/templates/instructor/instructor_dashboard_2/send_email.html @@ -15584,7 +15591,7 @@ msgid "" "Have you sent the email to yourself first to make sure you're happy with how" " it's displayed, and that embedded links and images work properly?" msgstr "" -"Vous êtes-vous d'abord envoyé l'email à vous-même pour vérifier si son " +"Vous êtes-vous d'abord envoyé l'e-mail à vous-même pour vérifier si son " "affichage est satisfaisant et si les liens et images fonctionnent " "correctement ?" @@ -15597,25 +15604,25 @@ msgid "" "Once the 'Send Email' button is clicked, your email will be queued for " "sending." msgstr "" -"Une fois que le bouton \"Envoyer l'email\" est cliqué, votre email sera mis " -"en file d'attente pour l'envoi." +"Une fois que le bouton \"Envoyer l'e-mail\" est cliqué, votre courrier sera " +"mis en file d'attente pour l'envoi." #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "A queued email CANNOT be cancelled." -msgstr "Un email mis en fil d'attente NE PEUT PAS être annulé." +msgstr "Un e-mail mis en fil d'attente NE PEUT PAS être annulé." #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "" "Email actions run in the background. The status for any active tasks - " "including email tasks - appears in a table below." msgstr "" -"Les actions liées aux emails fonctionnent en tâche de fond. Le statut des " -"tâches actives - incluant les actions email - apparaissent dans le tableau " +"Les actions liées aux e-mails fonctionnent en tâche de fond. L'état des " +"tâches actives - incluant les actions e-mail - apparaissent dans le tableau " "ci-dessous." #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Email Task History" -msgstr "Historique des actions liées aux emails" +msgstr "Historique des actions liées aux e-mails" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "To see the content of all previously sent emails, click this button:" @@ -15625,23 +15632,23 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Sent Email History" -msgstr "Historique des emails envoyés" +msgstr "Historique des e-mails envoyés" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "To read an email, click its subject." -msgstr "Pour lire un email, cliquer sur son sujet" +msgstr "Pour lire un e-mail, cliquer sur son sujet" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "" "To see the status for all bulk email tasks ever submitted for this course, " "click on this button:" msgstr "" -"Pour consulter l'état de toutes les tâches d'emailing réalisées pour ce " -"cours, cliquer sur ce bouton :" +"Pour consulter l'état de toutes les tâches de courrier électronique " +"réalisées pour ce cours, cliquer sur ce bouton :" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Show Email Task History" -msgstr "Afficher l'historique des tâches liées aux emails" +msgstr "Afficher l'historique des tâches liées aux e-mails" #: lms/templates/instructor/instructor_dashboard_2/set_course_mode_price_modal.html msgid "Set Course Mode Price" @@ -15940,7 +15947,7 @@ msgstr "" #: lms/templates/peer_grading/peer_grading_closed.html #: lms/templates/peer_grading/peer_grading_problem.html msgid "Peer Grading" -msgstr "Notation par les pairs" +msgstr "Evaluation par les pairs" #: lms/templates/peer_grading/peer_grading.html msgid "" @@ -16116,7 +16123,7 @@ msgid "" msgstr "" "Un problème est survenu. Merci de vérifier que l'URL entrée est correcte. Il" " arrive que cette URL soit divisée en deux lignes par votre fournisseur " -"d'email . Si le problème persiste, contactez-nous à l'adresse {email}." +"d'e-mail . Si le problème persiste, contactez-nous à l'adresse {email}." #: lms/templates/registration/activation_invalid.html msgid "Or you can go back to the {link_start}home page{link_end}." @@ -16131,9 +16138,9 @@ msgid "" "We've e-mailed you instructions for setting your password to the e-mail " "address you submitted. You should be receiving it shortly." msgstr "" -"Nous vous avons envoyé par mail des instructions pour initialiser votre mot " -"de passe à l'adresse email que vous avez fournie. Vous devriez bientôt les " -"recevoir." +"Nous vous avons envoyé par courrier des instructions pour initialiser votre " +"mot de passe à l'adresse e-mail que vous avez fournie. Vous devriez bientôt " +"les recevoir." #: lms/templates/shoppingcart/billing_details.html #: lms/templates/shoppingcart/receipt.html @@ -16192,10 +16199,21 @@ msgstr "Voir les Cours" msgid "Payment" msgstr "Paiement" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "Télécharger les rapports CSV" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "Télécharger les données CSV" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" +"Il y a une erreur dans le format de votre date. Elle doit être sous la forme" +" AAAA-MM-JJ" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "Ces rapports sont délimités par les dates de début et de fin." @@ -16516,14 +16534,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "Panier - {platform_name}" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "Vérifier" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "Confirmation" @@ -16709,111 +16723,15 @@ msgstr "" "Si vous avez une question à propos de ce cours ou de ce questionnaire, vous " "pouvez contacter {mail_to_link}." -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "Éditer votre nom" +#: lms/templates/verify_student/face_upload.html +msgid "Edit Your Full Name" +msgstr "Éditer votre nom complet" -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "The following error occurred while editing your name:" msgstr "" "L'erreur suivante s'est produite lors de la modification de votre nom :" -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"Pour maintenir la crédibilité des certificats {platform}, tous les " -"changements de nom seront loggés et enregistrés." - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "Motif de changement de nom :" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "Changer mon nom" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "Avez-vous des problèmes techniques ?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "Avez-vous des questions ?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"Merci de lire {a_start}notre FAQ afin de voir les questions fréquentes à " -"propos de nos certificats{a_end}." - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "Spécifications techniques" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" -"Veuillez vous assurer que votre navigateur est mis à jour à la{a_start} " -"version la plus récente possible{a_end}. Veuillez aussi vous assurer que " -"votre webcam est branchée, mise sous tension, et autorisé à " -"fonctionner dans votre navigateur Web (paramètre communément ajustable dans " -"les paramètres du navigateur). " - -#: lms/templates/verify_student/face_upload.html -msgid "Edit Your Full Name" -msgstr "Éditer votre nom complet" - #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -16844,389 +16762,58 @@ msgstr "Vérifié pour {course_name}" msgid "Enroll In {course_name}" msgstr "S'inscrire à {course_name}" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "Avez-vous des questions ?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"Merci de lire {a_start}notre FAQ afin de voir les questions fréquentes à " +"propos de nos certificats{a_end}." + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "Spécifications techniques" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" +"Veuillez vous assurer que votre navigateur est mis à jour à la{a_start} " +"version la plus récente possible{a_end}. Veuillez aussi vous assurer que " +"votre webcam est branchée, mise sous tension, et autorisé à " +"fonctionner dans votre navigateur Web (paramètre communément ajustable dans " +"les paramètres du navigateur). " + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "Re-vérification" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "Aucune Webcam détectée" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." -msgstr "" -"Il semble qu'aucune webcam ne soit connectée. Vérifiez que votre webcam est " -"connectée et fonctionnelle avant de continuer." - -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "Aucun Flash détecté" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"Il semble que Flash ne soit pas installé. {a_start} Obtenez Flash {a_end} " -"pour continuer votre inscription." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "Erreur lors de l’envoi de vos images" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"Oups ! Quelque chose s'est mal passé. Merci de confirmer vos détails, et " -"réessayer." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "Votre Progression" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "Étape en cours:" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "Reprenez la photo" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "Reprenez la photo d'identité " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "Reprenez votre photo" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"Utiliser votre webcam pour prendre une image de votre visage, afin que nous " -"puissions la comparer avec celle de votre ID." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"Vous ne voyez pas votre photo ? Assurez-vous que votre navigateur est " -"autorisé à utiliser votre webcam quand il le demande." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "Reprendre" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "Pas mal" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "Astuces pour prendre une photo convenable" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "Vérifiez que votre visage est bien éclairé" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "Vérifiez que votre visage est entièrement dans le cadre" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "" -"Peut-on vérifier la concordance entre la photo que vous avez prise et celle " -"de vos identifiants ?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "Questions fréquentes" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "Pourquoi avez-vous besoin de ma photo ?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" -"Dans le cadre du processus de vérification, nous avons besoin de votre photo" -" pour confirmer que vous êtes bien vous-même." - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "Que faites-vous avec cette photo ?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" -"Nous l'utilisons uniquement pour vérifier votre identité. Elle n'est jamais " -"affiché nulle part." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "" -"Dès que vous avez vérifié que votre photo vous convient, vous pouvez " -"procéder à l'étape 2." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "Allez à l'étape 2 : Reprendre une photo de pièce d'identité" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "Montrez-nous votre pièce d'identité" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"Utilisez votre webcam pour prendre une photo de votre pièce d'identité pour " -"que nous puissions vérifier sa concordance avec la photo et le nom de votre " -"compte." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "Assurez-vous que votre pièce d'identité est bien éclairée" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "Vérifiez qu'il n'y a pas de reflets" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "Assurez-vous de pouvoir voir votre photo et lire votre nom" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" -"Essayez de laisser vos doigts sur les bords pour éviter de masquer des " -"informations importantes" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "Pourquoi avez-vous besoin d'une photo de ma pièce d'identité ?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" -"Nous avons besoin d'établir la concordance entre votre pièce d'identité et " -"votre photo et votre nom pour confirmer que vous êtes bien vous." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"Nous le chiffrons et l'envoyons à notre service d'autorisation sécurisé pour" -" validation. Nous utilisons les plus hauts niveaux de sécurité et ne " -"conservons nulle part la photo ou les informations une fois que la " -"concordance est établie." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" -"Une fois que vous avez vérifié que la photo de votre pièce d'identité " -"convient bien, vous pouvez procéder à l'étape 3." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "Aller à l'étape 3 : vérification de vos informations" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "Vérifiez votre formulaire" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" -"Assurez-vous que nous pourrons vérifier votre identité avec les photos et " -"les informations ci-dessous." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "Vérifier les photos que vous avez reprises" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" -"Veuillez vérifier que les photos répondent aux prérequis listés ci-dessous." - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "La photo ci-dessus nécessite de répondre aux prérequis suivants :" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "Soyez bien éclairé" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "Montrez votre visage en entier" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "" -"La photo sur votre pièce d'identité doit concorder avec la photo de votre " -"visage" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "Soyez lisible (pas trop éloigné, sans reflet)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "" -"Le nom sur votre pièce d'identité doit concorder avec le nom dans votre " -"compte ci-dessous" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "Les photos ne répondent pas aux prérequis ?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "Reprenez vos photos" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "Vérifiez votre nom" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" -"Assurez-vous que le nom ({full_name}) figurant sur votre compte " -"{platform_name} concorde avec votre pièce d'identité. Nous allons également " -"utiliser ce nom sur votre certificat." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "Editez votre Nom" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"Une fois que vous avez vérifié que vos informations correspondent aux pré-" -"requis, vous pouvez avancer et confirmer votre demande de re-vérification." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "Oui ! Mes informations correspondent." - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "Vous devez vérifier une nouvelle fois pour continuer" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"Pour continuer dans la partie à identité vérifiée dans {course}, vous devez " -"vérifier à nouveau votre identité le {date}. Aller à l'URL." - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "Confirmation de la demande de revérification" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "Vos certificats ont été mis à jour" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"Nous avons obtenu vos nouvelles informations et nous allons les passer en " -"revue sous peu pour vérifier votre identité. Vous devriez recevoir une mise " -"à jour de votre statut dans les 1-2 jours. Entre temps, vous avez toujours " -"accès à tout le contenu de votre cours." - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "Retour à votre tableau de bord" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "Nouvelle vérification échouée" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"Votre nouvelle vérification a été soumise après la date limite, et vous ne " -"pouvez donc plus être revérifié." - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" -"Veuillez contacter le support si vous pensez que ce message est une erreur." - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(actif){span_end}" @@ -17366,7 +16953,7 @@ msgstr "Actions de la Page" #: cms/templates/asset_index.html cms/templates/videos_index.html msgid "Upload New File" -msgstr "Envoyer un Nouveau Fichier" +msgstr "Charger un nouveau fichier" #: cms/templates/asset_index.html msgid "Adding Files for Your Course" @@ -18904,14 +18491,14 @@ msgstr "Accueil {studio_name}" msgid "New Course" msgstr "Nouveau Cours" -#: cms/templates/index.html -msgid "New Library" -msgstr "Nouvelle Bibliothèque" - #: cms/templates/index.html msgid "Email staff to create course" msgstr "Envoyez un email à l'équipe pédagogique pour créer un cours" +#: cms/templates/index.html +msgid "New Library" +msgstr "Nouvelle Bibliothèque" + #: cms/templates/index.html msgid "Please correct the highlighted fields below." msgstr "Merci de corriger les champs en surbrillance ci-dessous." @@ -19584,8 +19171,6 @@ msgstr "Information de base" msgid "The nuts and bolts of your course" msgstr "Les références de votre Cours" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "Ce champ est désactivé : Cette information ne peut pas être changée" @@ -19648,7 +19233,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -19803,7 +19398,7 @@ msgstr "" #: cms/templates/settings.html msgid "Upload Course Image" -msgstr "Télécharger l'image du cours" +msgstr "Charger l'image du cours" #: cms/templates/settings.html msgid "Course Introduction Video" @@ -20414,6 +20009,10 @@ msgstr "" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "Il s'agit d'une chaine de texte" diff --git a/conf/locale/fr/LC_MESSAGES/djangojs.mo b/conf/locale/fr/LC_MESSAGES/djangojs.mo index bdd2878e7c..1479ecf4aa 100644 Binary files a/conf/locale/fr/LC_MESSAGES/djangojs.mo and b/conf/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/fr/LC_MESSAGES/djangojs.po b/conf/locale/fr/LC_MESSAGES/djangojs.po index f6eb54f523..1e2340c250 100644 --- a/conf/locale/fr/LC_MESSAGES/djangojs.po +++ b/conf/locale/fr/LC_MESSAGES/djangojs.po @@ -21,7 +21,7 @@ # Julien Colmonts , 2014 # Julien Rolland , 2013 # Kévin SEVERIN , 2015 -# laurentFUN , 2014 +# laurentFUN , 2014-2015 # moocit-france , 2015 # Manjari , 2014 # Olivier Lebon , 2015 @@ -40,7 +40,7 @@ # SLOG90 , 2014 # Tahar , 2015 # Xavier Antoviaque , 2014 -# yepelboin , 2014 +# yepelboin , 2014-2015 # #-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-# # edX translation file. # Copyright (C) 2015 EdX @@ -103,13 +103,14 @@ # moocit-france , 2015 # rafcha , 2014-2015 # Steven BERNARD , 2014 +# yepelboin , 2015 msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" -"Last-Translator: Sarina Canelake \n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-29 09:46+0000\n" +"Last-Translator: yepelboin \n" "Language-Team: French (http://www.transifex.com/projects/p/edx-platform/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -170,13 +171,6 @@ msgstr "OK" msgid "Cancel" msgstr "Annuler" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "" -"Ce lien s'ouvrira dans une nouvelle fenêtre ou un nouvel onglet de votre " -"navigateur" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -200,7 +194,7 @@ msgstr "Supprimer" #: cms/static/js/models/active_video_upload.js cms/static/js/views/assets.js #: lms/static/js/views/fields.js msgid "Uploading" -msgstr "Envoi en cours" +msgstr "Chargement en cours" #. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the @@ -316,6 +310,10 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "(%(num_points)s point possible)" msgstr[1] "(%(num_points)s points possibles)" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "Réponse:" @@ -338,7 +336,7 @@ msgstr "Réponse cachée" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Status: unsubmitted" -msgstr "État : non soumis" +msgstr "" #. Translators: A "rating" is a score a student gives to indicate how well #. they feel they were graded on this problem @@ -2531,7 +2529,7 @@ msgstr "Nom d'utilisateur" #: lms/static/coffee/src/instructor_dashboard/membership.js msgid "Email" -msgstr "E-mail" +msgstr "Email" #: lms/static/coffee/src/instructor_dashboard/membership.js msgid "Revoke access" @@ -2539,11 +2537,11 @@ msgstr "Retirer l'accès" #: lms/static/coffee/src/instructor_dashboard/membership.js msgid "Enter username or email" -msgstr "Veuillez saisir un nom d'utilisateur ou un e-mail." +msgstr "Veuillez saisir un nom d'utilisateur ou un email." #: lms/static/coffee/src/instructor_dashboard/membership.js msgid "Please enter a username or email." -msgstr "Veuillez saisir un nom d'utilisateur ou un e-mail." +msgstr "Veuillez saisir un nom d'utilisateur ou un email." #: lms/static/coffee/src/instructor_dashboard/membership.js msgid "Error changing user's permissions." @@ -3127,7 +3125,7 @@ msgstr "Nom de l'exercice" #: lms/static/coffee/src/staff_grading/staff_grading.js msgid "Graded" -msgstr "Evalué" +msgstr "Noté" #: lms/static/coffee/src/staff_grading/staff_grading.js msgid "Available to Grade" @@ -3673,7 +3671,7 @@ msgid "" "The name that appears on your certificates. Other learners never see your " "full name." msgstr "" -"Le nom complet visible sur vos certificats. Les autres étudiants ne pourrons" +"Le nom complet visible sur vos certificats. Les autres étudiants ne pourront" " pas voir votre nom complet." #: lms/static/js/student_account/views/account_settings_factory.js @@ -3934,6 +3932,18 @@ msgstr "" msgid "Could not retrieve payment information" msgstr "Impossible de récupérer les informations de paiement" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "Confirmer" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "Une erreur est survenue. Essayez de rafraîchir la page." @@ -4006,13 +4016,8 @@ msgstr "Modifier l'image" #: lms/static/js/views/fields.js #: cms/templates/js/video/metadata-translations-item.underscore -#, fuzzy msgid "Remove" -msgstr "" -"#-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-#\n" -"Supprimer\n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"Enlever" +msgstr "Supprimer" #: lms/static/js/views/fields.js msgid "Removing" @@ -4128,6 +4133,12 @@ msgstr "Suppression en cours" msgid "OpenAssessment Save Error" msgstr "Erreur de la sauvegarde d'OpenAssessment" +#: cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" +"Ce lien s'ouvrira dans une nouvelle fenêtre ou un nouvel onglet de votre " +"navigateur" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "Ce lien s'ouvrira dans une nouvelle fenêtre contextuelle" @@ -4356,7 +4367,7 @@ msgstr "Mis en file d'attente" #. completed successfully #: cms/static/js/models/active_video_upload.js msgid "Upload completed" -msgstr "Téléversement terminé" +msgstr "Chargement terminé" #. Translators: This is the status of a video upload that has failed #: cms/static/js/models/active_video_upload.js @@ -4535,13 +4546,8 @@ msgstr "Choisir le fichier" #: cms/static/js/views/assets.js cms/static/js/views/assets.js.c #: cms/templates/js/asset-upload-modal.underscore -#, fuzzy msgid "Upload New File" -msgstr "" -"#-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-#\n" -"Envoyer un nouveau fichier\n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"Téléversez un nouveau fichier" +msgstr "Charger un nouveau fichier" #: cms/static/js/views/assets.js cms/static/js/views/assets.js.c msgid "Load Another File" @@ -4599,7 +4605,7 @@ msgstr "Traitement de la demande de relance" #: cms/static/js/views/edit_chapter.js msgid "Upload a new PDF to “<%= name %>”" -msgstr "Uploader un nouveau PDF appelé \"<%= name %>”" +msgstr "Charger un nouveau PDF appelé \"<%= name %>”" #: cms/static/js/views/edit_chapter.js msgid "Please select a PDF file to upload." @@ -4816,13 +4822,8 @@ msgstr "" #: cms/static/js/views/uploads.js #: cms/templates/js/metadata-file-uploader-item.underscore #: cms/templates/js/video/metadata-translations-item.underscore -#, fuzzy msgid "Upload" -msgstr "" -"#-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-#\n" -"Envoyer\n" -"#-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-#\n" -"Charger" +msgstr "Charger" #: cms/static/js/views/uploads.js msgid "We're sorry, there was an error" @@ -5580,6 +5581,45 @@ msgstr "Une erreur est survenue. Merci de rafraîchir la page." msgid "Forgot password?" msgstr "Mot de passe oublié?" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "Se connecter" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5607,15 +5647,15 @@ msgstr "Nous n'avons pas pu vous connecter." msgid "An error occurred when signing you in to %(platformName)s." msgstr "" -#: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "Se connecter" - #: lms/templates/student_account/login.underscore msgid "or sign in with" msgstr "ou se connecter avec" +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "New to %(platformName)s?" msgstr "Nouveau sur %(platformName)s?" @@ -6043,6 +6083,22 @@ msgstr "" "Un permis de conduire, un passeport ou toute pièce d'identité avec votre nom" " et photo." +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "" @@ -6104,10 +6160,6 @@ msgstr "" msgid "Before proceeding, please confirm that your details match" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "Confirmer" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " @@ -6132,7 +6184,7 @@ msgstr "Glisser déposer ou cliquer ici pour importer des fichiers vidéo." #: cms/templates/js/active-video-upload.underscore msgid "status" -msgstr "état" +msgstr "statut" #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" @@ -6378,7 +6430,7 @@ msgstr "Glissez pour modifier l'ordre" #: cms/templates/js/course-outline.underscore msgid "Release Status:" -msgstr "État de la diffusion :" +msgstr "Statut de la diffusion :" #: cms/templates/js/course-outline.underscore #: cms/templates/js/publish-xblock.underscore @@ -6521,7 +6573,7 @@ msgstr "" #: cms/templates/js/edit-chapter.underscore msgid "Upload PDF" -msgstr "Téléverser un PDF" +msgstr "Envoyer un PDF" #: cms/templates/js/edit-chapter.underscore msgid "delete chapter" @@ -7025,12 +7077,12 @@ msgstr "" #: cms/templates/js/video/transcripts/messages/transcripts-uploaded.underscore #: cms/templates/js/video/transcripts/messages/transcripts-use-existing.underscore msgid "Upload New Transcript" -msgstr "Téléverser une nouvelle transcription" +msgstr "Envoyer une nouvelle transcription" #: cms/templates/js/video/transcripts/messages/transcripts-found.underscore #: cms/templates/js/video/transcripts/messages/transcripts-import.underscore msgid "Upload New .srt Transcript" -msgstr "Téléverser une nouvelle transcription .srt" +msgstr "Envoyer une nouvelle transcription .srt" #: cms/templates/js/video/transcripts/messages/transcripts-found.underscore #: cms/templates/js/video/transcripts/messages/transcripts-not-found.underscore diff --git a/conf/locale/he/LC_MESSAGES/django.mo b/conf/locale/he/LC_MESSAGES/django.mo index cd16dc4ed5..56a45e0e03 100644 Binary files a/conf/locale/he/LC_MESSAGES/django.mo and b/conf/locale/he/LC_MESSAGES/django.mo differ diff --git a/conf/locale/he/LC_MESSAGES/django.po b/conf/locale/he/LC_MESSAGES/django.po index 5926fd6658..04bd1da852 100644 --- a/conf/locale/he/LC_MESSAGES/django.po +++ b/conf/locale/he/LC_MESSAGES/django.po @@ -60,7 +60,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" "PO-Revision-Date: 2015-05-28 20:00+0000\n" "Last-Translator: Nadav Stark \n" "Language-Team: Hebrew (http://www.transifex.com/projects/p/edx-platform/language/he/)\n" @@ -679,12 +679,27 @@ msgstr "" msgid "Unable to send email activation link. Please try again later." msgstr "" -#: common/djangoapps/student/views.py -msgid "Name required" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." msgstr "" #: common/djangoapps/third_party_auth/pipeline.py @@ -1131,6 +1146,24 @@ msgstr "" msgid "processing" msgstr "" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -1943,6 +1976,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "" @@ -2791,13 +2834,6 @@ msgstr "" msgid "General" msgstr "" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "" @@ -5736,7 +5772,6 @@ msgstr "" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "" @@ -6532,6 +6567,7 @@ msgid "Verified Enrollment" msgstr "" #: lms/djangoapps/shoppingcart/reports.py +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue" msgstr "" @@ -6572,7 +6608,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "" @@ -7175,17 +7210,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -7289,7 +7313,6 @@ msgid "Payment confirmation" msgstr "" #: lms/djangoapps/verify_student/views.py -#: lms/templates/verify_student/photo_reverification.html msgid "Take photo" msgstr "" @@ -8440,6 +8463,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -8698,7 +8729,6 @@ msgid "Loading" msgstr "טוען" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "" @@ -8838,11 +8868,6 @@ msgstr "" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -8871,10 +8896,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -8883,6 +8904,10 @@ msgstr "" msgid "Sorry, there was an error when trying to enroll you" msgstr "" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -11587,16 +11612,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -11612,8 +11641,7 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" +msgid "Less" msgstr "" #: lms/templates/courseware/progress.html @@ -11882,6 +11910,37 @@ msgid "" "{unenroll_link_start}unenroll{unenroll_link_end} from this course" msgstr "" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "" @@ -12353,6 +12412,10 @@ msgstr "" msgid "Open" msgstr "" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -14865,10 +14928,19 @@ msgstr "" msgid "Payment" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "" @@ -15183,14 +15255,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "" @@ -15361,101 +15429,14 @@ msgid "" " href=\"{mail_to_link}\"\">{mail_to_link}." msgstr "" -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "הסיבה לשינוי השם:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -15486,337 +15467,51 @@ msgstr "" msgid "Enroll In {course_name}" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "" @@ -17321,11 +17016,11 @@ msgid "New Course" msgstr "" #: cms/templates/index.html -msgid "New Library" +msgid "Email staff to create course" msgstr "" #: cms/templates/index.html -msgid "Email staff to create course" +msgid "New Library" msgstr "" #: cms/templates/index.html @@ -17950,8 +17645,6 @@ msgstr "" msgid "The nuts and bolts of your course" msgstr "" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "" @@ -18011,7 +17704,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -18731,6 +18434,10 @@ msgstr "" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "זוהי שורת מפתח" diff --git a/conf/locale/he/LC_MESSAGES/djangojs.mo b/conf/locale/he/LC_MESSAGES/djangojs.mo index d57e581947..37ba37314c 100644 Binary files a/conf/locale/he/LC_MESSAGES/djangojs.mo and b/conf/locale/he/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/he/LC_MESSAGES/djangojs.po b/conf/locale/he/LC_MESSAGES/djangojs.po index a91246d3c2..7fa04a7ad4 100644 --- a/conf/locale/he/LC_MESSAGES/djangojs.po +++ b/conf/locale/he/LC_MESSAGES/djangojs.po @@ -44,8 +44,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-26 19:41+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Hebrew (http://www.transifex.com/projects/p/edx-platform/language/he/)\n" "MIME-Version: 1.0\n" @@ -107,11 +107,6 @@ msgstr "" msgid "Cancel" msgstr "" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -253,6 +248,10 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "" msgstr[1] "" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "תשובה:" @@ -3659,6 +3658,19 @@ msgstr "" msgid "Could not retrieve payment information" msgstr "" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -3837,6 +3849,10 @@ msgstr "" msgid "OpenAssessment Save Error" msgstr "" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "" @@ -4104,6 +4120,9 @@ msgid "" msgstr "" #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "" @@ -5209,6 +5228,45 @@ msgstr "" msgid "Forgot password?" msgstr "" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5235,12 +5293,12 @@ msgid "An error occurred when signing you in to %(platformName)s." msgstr "" #: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" +msgid "or sign in with" msgstr "" #: lms/templates/student_account/login.underscore -msgid "or sign in with" +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" msgstr "" #: lms/templates/student_account/login.underscore @@ -5647,6 +5705,22 @@ msgid "" "photo." msgstr "" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "" @@ -5706,10 +5780,6 @@ msgstr "" msgid "Before proceeding, please confirm that your details match" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/hi/LC_MESSAGES/django.mo b/conf/locale/hi/LC_MESSAGES/django.mo index f8555e3e9c..d90d62f2a9 100644 Binary files a/conf/locale/hi/LC_MESSAGES/django.mo and b/conf/locale/hi/LC_MESSAGES/django.mo differ diff --git a/conf/locale/hi/LC_MESSAGES/django.po b/conf/locale/hi/LC_MESSAGES/django.po index c3826c1bea..0fba72db31 100644 --- a/conf/locale/hi/LC_MESSAGES/django.po +++ b/conf/locale/hi/LC_MESSAGES/django.po @@ -72,8 +72,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" -"PO-Revision-Date: 2015-05-28 20:00+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" +"PO-Revision-Date: 2015-06-28 20:21+0000\n" "Last-Translator: ria1234 \n" "Language-Team: Hindi (http://www.transifex.com/projects/p/edx-platform/language/hi/)\n" "MIME-Version: 1.0\n" @@ -146,13 +146,8 @@ msgstr "" #: lms/djangoapps/shoppingcart/reports.py #: lms/templates/open_ended_problems/open_ended_problems.html #: lms/templates/shoppingcart/receipt.html -#, fuzzy msgid "Status" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"स्थिति\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"स्थिति:" +msgstr "स्थिति" #: common/djangoapps/config_models/admin.py msgid "Revert to the selected configuration" @@ -586,13 +581,8 @@ msgid "Too many failed login attempts. Try again later." msgstr "लॉग-इन के कई प्रयास विफल रहे। थोड़ी देर बाद में फिर से कोशिश करें।" #: common/djangoapps/student/views.py lms/templates/provider_login.html -#, fuzzy msgid "Email or password is incorrect." -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"ई-मेल या पासवर्ड गलत है।\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"ई-मेल या पासवर्ड ग़लत है।" +msgstr "ई-मेल या पासवर्ड गलत है।" #: common/djangoapps/student/views.py msgid "" @@ -697,13 +687,28 @@ msgstr "इस ई-मेल से जुड़ा हुआ एक खात msgid "Unable to send email activation link. Please try again later." msgstr "" -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "नाम आवश्यक है" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "अवैध आईडी" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1149,6 +1154,24 @@ msgstr "जवाब नहीं दिया हुआ" msgid "processing" msgstr "" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -1975,6 +1998,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "TBD" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "" @@ -2822,13 +2855,6 @@ msgstr "" msgid "General" msgstr "सामान्य" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "TBD" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "" @@ -4972,13 +4998,8 @@ msgid "Courses loaded in the modulestore" msgstr "पाठ्यक्रमों को मॉड्यूल स्टोर में लोड कर दिया गया" #: lms/djangoapps/dashboard/sysadmin.py lms/templates/tracking_log.html -#, fuzzy msgid "username" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"उपयोगकर्ता\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"उपयोगकर्तानाम" +msgstr "उपयोगकर्ता" #: lms/djangoapps/dashboard/sysadmin.py msgid "email" @@ -5799,7 +5820,6 @@ msgstr "" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "पूरा नाम " @@ -5885,13 +5905,8 @@ msgstr "रीसेट करें" #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py #: lms/templates/wiki/plugins/attachments/index.html wiki/models/article.py -#, fuzzy msgid "deleted" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"नष्ट कर दिया गया\n" -"#-#-#-#-# wiki.po (edx-platform) #-#-#-#-#\n" -"नष्ट किया" +msgstr "नष्ट कर दिया गया" #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. @@ -6657,7 +6672,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "शॉपिंग कार्ट में कुछ डाल पाने के लिए आपका लॉग-इन होना ज़रूरी है " #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "आपने जिस पाठ्यक्रम के लिए अनुरोध किया है वह मौजूद नहीं है" @@ -7320,24 +7334,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -#, fuzzy -msgid "Download CSV Reports" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"सी एस वी रिपोर्टें डाउनलोड करें\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"CSV रिपोर्टें डाउनलोड करें" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"तिथि के इनपुट में कोई त्रुटि हुई थी। तिथि YYYY-MM-DD के रूप में भरी जानी " -"चाहिए।" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -7442,9 +7438,9 @@ msgstr "" msgid "Payment confirmation" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/djangoapps/verify_student/views.py msgid "Take photo" -msgstr "तस्वीर लें" +msgstr "" #: lms/djangoapps/verify_student/views.py msgid "Take a photo of your ID" @@ -7934,13 +7930,8 @@ msgstr "पूर्वावलोकन" #: lms/templates/instructor/instructor_dashboard_2/set_course_mode_price_modal.html #: lms/templates/modal/_modal-settings-language.html #: lms/templates/modal/accessible_confirm.html -#, fuzzy msgid "Close" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"बंद करें\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"बंद करे" +msgstr "बंद करें" #: lms/templates/wiki/edit.html msgid "Wiki Preview" @@ -8631,6 +8622,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -8885,7 +8884,6 @@ msgid "Loading" msgstr "" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "बंद करें" @@ -9027,11 +9025,6 @@ msgstr "सहायता" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -9060,10 +9053,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -9072,6 +9061,10 @@ msgstr "" msgid "Sorry, there was an error when trying to enroll you" msgstr "" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -11858,16 +11851,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -11883,8 +11880,7 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" +msgid "Less" msgstr "" #: lms/templates/courseware/progress.html @@ -12178,6 +12174,37 @@ msgid "" "{unenroll_link_start}unenroll{unenroll_link_end} from this course" msgstr "" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "बदलें" @@ -12657,6 +12684,10 @@ msgstr "" msgid "Open" msgstr "" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -15283,10 +15314,21 @@ msgstr "" msgid "Payment" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "CSV रिपोर्टें डाउनलोड करें" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "CSV डेटा डाउनलोड करें" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" +"तिथि के इनपुट में कोई त्रुटि हुई थी। तिथि YYYY-MM-DD के रूप में भरी जानी " +"चाहिए।" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "इन रिपोर्टों को प्रारंभ और समाप्ति दिनांक द्वारा सीमांकित किया गया है" @@ -15605,14 +15647,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "समीक्षा" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "पुष्टिकरण" @@ -15789,105 +15827,14 @@ msgid "" " href=\"{mail_to_link}\"\">{mail_to_link}." msgstr "" -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "अपना नाम बदलें" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "आपका नाम बदलते समय ये त्रुटियां आईं:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"{platform} प्रमाण पत्रों की विश्वस्नीयता को बनाने रखने के लिए, हर नाम में " -"किए गए परिवर्तन को रिकॉर्ड किया जाएगा।" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "नाम बदलने की वजह:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "मेरा नाम बदलें" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "कोई तकनीकी परेशानी हो रही है?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "आपके कोई सवाल हैं?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"कृपया {a_start}प्रमाण पत्रों के बारे में कुछ आम सवाल देखने के लिए हमारे " -"अक़्सर पूछे गए सवाल{a_end} देखें।" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "तकनीकी आवश्यकताएं" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "अपना पूरा नाम बदलें" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "आपका नाम बदलते समय ये त्रुटियां आईं:" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -15918,380 +15865,53 @@ msgstr "" msgid "Enroll In {course_name}" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "आपके कोई सवाल हैं?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"कृपया {a_start}प्रमाण पत्रों के बारे में कुछ आम सवाल देखने के लिए हमारे " +"अक़्सर पूछे गए सवाल{a_end} देखें।" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "तकनीकी आवश्यकताएं" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "पुनः वैरीफीकेशन" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "कोई वैबकैमरा नहीं मिला" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." -msgstr "" -"आपने शायद कोई वैबकैमरा कनेक्ट नहीं किया हुआ है। दोबारा जांच लें कि आपके " -"वैबकैमरे की तारें ठीक तरह से लगी हुई हैं और तब रजिस्टर करना ज़ारी करें।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "कोई फ़्लैश नहीं मिली" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"शायद आपने फ़्लैश नहीं लगाई हुई है। अपनी रजिस्ट्रेशन ज़ारी रखने के लिए " -"{a_start} फ़्लैश लें {a_end} पर क्लिक करें।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "आपकी तस्वीरों को जमा करने में गड़बड़ " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"ओह! कुछ गड़बड़ हो गई। अपने द्वारा दी गई सारी सूचनाओं को ठीक से जांच लें और " -"फिर से कोशिश करें।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "आपकी प्रगति" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "मौजूदा कदम:" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "तस्वीर फिर से लें" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "अपने आई डी की तस्वीर फिर से लें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "अपनी तस्वीर फिर से लें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"अपने वेबकैमरे का उपयोग करके अपने चेहरे की तस्वीर लें ताकि हम तस्वीर का आपके " -"आईडी पर दी गई तस्वीर से मेल कर सकें।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"आपको अपनी तस्वीर नहीं दिख रही? पक्का करें कि जब आपका ब्राउज़र आपसे कैमरे को " -"उपयोग करने की अनुमति मांगता है तो आप उसे वह देते हैं। " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "फिर से लें " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "अच्छी लग रही है" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "सफ़ल फ़ोटो लेने के सुझाव" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "पक्का करें की आपका चेहरा साफ़-साफ़ दिखाई दे रहा है" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "पक्का करें की आपका पूरा चेहरा फ्रेम के अंदर है" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "क्या हम ली गई तस्वीर का आपका आईडी से मेल कर सकते हैं?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "आम पूछे गये सवाल" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "आपको मेरी तस्वीर की ज़रूरत क्यों है?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" -"वैरिफ़िकेशन के लिए हमें आपकी तस्वीर की ज़रूरत है यह आश्वस्त करने के लिए कि " -"आप आप ही हैं।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "आप इस तस्वीर के साथ क्या करते हैं?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" -"हम केवल पहचान वैरिफ़ाई करने के लिए इसका इस्तेमाल करते हैं। यह कहीं और नहीं " -"दिखाई जाती।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "" -"एक बार जब आप यह वैरीफाई कर लें कि आप की तस्वीर अच्छी लग रही है तब आप स्टेप 2" -" पर जा सकते हैं।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "चरण 2 पर जाएं: फ़ोटो आईडी फिर से लें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "हमें अपना आईडी दिखाएं" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"अपने वैब कैमरा से अपने आईडी की फोटो लें ताकि हम इसका मेल आपकी तस्वीर और आपके" -" खाते पर दिए गए आपके नाम से कर सकें।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "पक्का कर लें कि आपका आईडी अच्छी तरह से दिखाई दे रहा है" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "जांच करें कि उस पर कोई चमक नहीं हो" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "पक्का कर लें कि आप अपनी फोटो को ठीक से देख और नाम को पढ़ सकते हैं" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" -"अपनी उंगलियों को किनारे पर रखने की कोशिश करें जिससे कि कोई महत्वपूर्ण " -"जानकारी छुप न जाए" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "आपको मेरी आईडी की तस्वीर की जरूरत क्यों है?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" -"हमें आपके आईडी को आपके फोटो और नाम से मिलाने की ज़रूरत है ताकि हमें यह " -"आश्वासन हो जाए कि आप आप ही हैं।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"हम इसको एक कोड में डाल कर अपनी सुरक्षित प्राधिकरण सेवा को समीक्षा के लिए " -"भेजते हैं। हम कड़ी और ऊंचे, स्तर की सुरक्षा का उपयोग करते हैं और मेल होने के" -" बाद फोटो और सूचना को नष्ट कर देते हैं।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" -"एक बार जब आप अपने आईडी की तस्वीर से खुश हो जाएं तब आप स्टेप 3 पर जा सकते " -"हैं।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "सटेप 3 पर जाएं: अपनी जानकारी को दोबारा देखें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "जमा करने से पहले वैरीफाई करें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" -"यह पक्का कर लें कि हम आपकी नीचे दी गई फ़ोटो और आपके द्वारा दी गई जानकारी से " -"आपकी पहचान स्थापित कर सकें।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "आपने जो तस्वीरें ली हैं उनकी समीक्षा करें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" -"कृपया फ़ोटोएं ठीक से जांच लें और ध्यान से देख लें कि वे नीचे दी गई सारी " -"आवश्यकताओं को पूरा करती हैं।" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "ऊपर दी गई तस्वीर को नीचे दी गई आवश्यकताओं को पूरा करने की जरूरत है:" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "चेहरा स्पष्ट रूप से दिखाएं" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "अपना पूरा चेहरा दिखाएं" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "आपके आईडी की फोटो का आपके चेहरे की फ़ोटो से मेल खाना ज़रूरी हैं" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "पाठनीय (ना ज़्यादा दूर, ना ही कोई चमक)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "" -"आपके आईडी में जो नाम है उसका नीचे दिए गए खाते के नाम से मेल खाना ज़रूरी है" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "तस्वीरें आवश्यकताओं को पूरा नहीं कर रही हैं?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "अपनी तस्वीरें फिर से खींचें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "अपने नाम की जांच करें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "अपना नाम बदलें" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"एक बार जब आप यह वेरीफ़ाई कर लेते हैं कि दी गई जानकारी आवश्यकताओं से मैच करती" -" है तो आप अपनी पुनः वेरिफ़िकेशन की अर्जी की पुष्टि कर सकते हैं। " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "जी! मेरे द्वारा दी गई पूरी जानकारी सही हैं।" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "आगे बढ़ने के लिए आपका फिर से वेरीफ़ाई करना ज़रूरी है" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"{course} में आईडी वैरीफ़ाईड ट्रैक में आगे बड़ने के लिए, आपको {date} तक अपनी " -"पहचान फिर से वैरीफ़ाई करने की ज़रूरत है। इस यू आर एल पर जाएं." - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "प्रस्तुति की पुनः वेरिफ़िकेशन की पुष्टि" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "आपकी परिचय जानकारी अपडेट कर दी गई है " - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"जो जानकारी आपने अभी सबमिट की थी वह हमें मिल गई हैं और हम जल्द ही उसको " -"समीक्षा करेंगे ताकि उससे हम आपकी पहचान बैठा सकें। आपको 1-2 दिन में आपके " -"वैरिफ़िकेशन स्थिति के बारे में बताने का एक अपडेट मिलेगा। इस बीच, आप अपने " -"पाठ्यक्रम के पूरे कंटेंट को देख सकते है और एक्सेस कर सकते हैं।" - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "अपने डैशबोर्ड पर लौटें" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "पुन: वैरीफ़िकेशन विफल" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"आपका पुनः वैरीफ़िकेशन री-वैरीफ़िकेशन की समय सीमा के बाद प्रस्तुत किया गया " -"था, और अब आपको री-वैरीफ़ाई नहीं किया जा सकता।" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" -"अगर आपको लगता है की यह संदेश त्रुटि में है तो कृपया सहायता से संपर्क करें।" - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(active){span_end}" @@ -17796,11 +17416,11 @@ msgid "New Course" msgstr "" #: cms/templates/index.html -msgid "New Library" +msgid "Email staff to create course" msgstr "" #: cms/templates/index.html -msgid "Email staff to create course" +msgid "New Library" msgstr "" #: cms/templates/index.html @@ -18425,8 +18045,6 @@ msgstr "" msgid "The nuts and bolts of your course" msgstr "" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "" @@ -18486,7 +18104,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -19206,6 +18834,10 @@ msgstr "" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "यह एक प्रमुख स्ट्रिंग है" diff --git a/conf/locale/hi/LC_MESSAGES/djangojs.mo b/conf/locale/hi/LC_MESSAGES/djangojs.mo index 47622f9a4b..2345d00648 100644 Binary files a/conf/locale/hi/LC_MESSAGES/djangojs.mo and b/conf/locale/hi/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/hi/LC_MESSAGES/djangojs.po b/conf/locale/hi/LC_MESSAGES/djangojs.po index 160e6228bc..2a500673a8 100644 --- a/conf/locale/hi/LC_MESSAGES/djangojs.po +++ b/conf/locale/hi/LC_MESSAGES/djangojs.po @@ -46,8 +46,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-28 20:21+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Hindi (http://www.transifex.com/projects/p/edx-platform/language/hi/)\n" "MIME-Version: 1.0\n" @@ -109,10 +109,6 @@ msgstr "ठीक" msgid "Cancel" msgstr "रद्द करें" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -msgid "This link will open in a new browser window/tab" -msgstr "यह लिंक एक नए ब्राउज़र विंडो / टैब में खुलेगी" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -252,6 +248,10 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "(%(num_points)s संभव अंक)" msgstr[1] "(%(num_points)s संभव अंक)" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "" @@ -3706,6 +3706,19 @@ msgstr "" msgid "Could not retrieve payment information" msgstr "" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -3888,6 +3901,10 @@ msgstr "नष्ट हो रहा है" msgid "OpenAssessment Save Error" msgstr "खुले मूल्‍यांकन को जमा करने में त्रुटि प्राप्‍त हुई" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "यह लिंक एक मॉडल विंडो टैब में खुलेगी" @@ -5280,6 +5297,45 @@ msgstr "" msgid "Forgot password?" msgstr "" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5306,12 +5362,12 @@ msgid "An error occurred when signing you in to %(platformName)s." msgstr "" #: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" +msgid "or sign in with" msgstr "" #: lms/templates/student_account/login.underscore -msgid "or sign in with" +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" msgstr "" #: lms/templates/student_account/login.underscore @@ -5718,6 +5774,22 @@ msgid "" "photo." msgstr "" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "" @@ -5777,10 +5849,6 @@ msgstr "" msgid "Before proceeding, please confirm that your details match" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/ko_KR/LC_MESSAGES/django.mo b/conf/locale/ko_KR/LC_MESSAGES/django.mo index 54da9c6627..732d7efb5d 100644 Binary files a/conf/locale/ko_KR/LC_MESSAGES/django.mo and b/conf/locale/ko_KR/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ko_KR/LC_MESSAGES/django.po b/conf/locale/ko_KR/LC_MESSAGES/django.po index 7c2aa2f296..40544af37f 100644 --- a/conf/locale/ko_KR/LC_MESSAGES/django.po +++ b/conf/locale/ko_KR/LC_MESSAGES/django.po @@ -80,7 +80,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" "PO-Revision-Date: 2015-05-28 20:00+0000\n" "Last-Translator: Kevin Min \n" "Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/edx-platform/language/ko_KR/)\n" @@ -690,12 +690,27 @@ msgstr "" msgid "Unable to send email activation link. Please try again later." msgstr "" -#: common/djangoapps/student/views.py -msgid "Name required" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." msgstr "" #: common/djangoapps/third_party_auth/pipeline.py @@ -1141,6 +1156,24 @@ msgstr "" msgid "processing" msgstr "" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -1941,6 +1974,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "" @@ -2788,13 +2831,6 @@ msgstr "" msgid "General" msgstr "" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "" @@ -5699,7 +5735,6 @@ msgstr "" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "" @@ -6492,6 +6527,7 @@ msgid "Verified Enrollment" msgstr "" #: lms/djangoapps/shoppingcart/reports.py +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue" msgstr "" @@ -6532,7 +6568,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "" @@ -7135,17 +7170,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -7248,9 +7272,9 @@ msgstr "" msgid "Payment confirmation" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/djangoapps/verify_student/views.py msgid "Take photo" -msgstr "사진 촬영" +msgstr "" #: lms/djangoapps/verify_student/views.py msgid "Take a photo of your ID" @@ -8388,6 +8412,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -8646,7 +8678,6 @@ msgid "Loading" msgstr "" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "" @@ -8786,11 +8817,6 @@ msgstr "" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -8819,10 +8845,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -8831,6 +8853,10 @@ msgstr "" msgid "Sorry, there was an error when trying to enroll you" msgstr "" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -11546,16 +11572,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -11571,8 +11601,7 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" +msgid "Less" msgstr "" #: lms/templates/courseware/progress.html @@ -11840,6 +11869,37 @@ msgid "" "{unenroll_link_start}unenroll{unenroll_link_end} from this course" msgstr "" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "편집" @@ -12311,6 +12371,10 @@ msgstr "" msgid "Open" msgstr "" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -14833,10 +14897,19 @@ msgstr "" msgid "Payment" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "" @@ -15149,14 +15222,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "검토" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "확인" @@ -15329,101 +15398,14 @@ msgid "" " href=\"{mail_to_link}\"\">{mail_to_link}." msgstr "" -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "이름 편집" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr " {platform} 수료증의 신뢰성을 유지하기 위해 모든 이름 변경은 로그되고 기록될 것입니다." - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "이름 변경 사유 :" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "이름 변경" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "기술적인 문제가 있으십니까?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "질문이 있습니까?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "인증서 {a_end}에 대해 자주 묻는 질문 코멘트를 보기 위해서는 {a_start} FAQ를 읽어 보시기 바랍니다." - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "이름 편집" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -15454,339 +15436,51 @@ msgstr "" msgid "Enroll In {course_name}" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "질문이 있습니까?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "인증서 {a_end}에 대해 자주 묻는 질문 코멘트를 보기 위해서는 {a_start} FAQ를 읽어 보시기 바랍니다." + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "웹캠 미감지" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "플래시 미감지" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "플래시가 설치되지 않았습니다. 등록절차를 위해서 {a_start} 플래시를 구하시길 바랍니다. {a_end} " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "진도" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "현재 단계:" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "자신의 얼굴을 촬영하기 위해 웹캠을 사용하십시요. 그러면 당신 아이디에 있는 사진과 비교할 것입니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "사진이 보이지 않습니까? 카메라 사용을 위한 브라우저를 확인바랍니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "다시 촬영" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "좋게 보입니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "좋은 사진 촬영법" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "얼굴이 잘 조명받도록 하세요" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "얼굴 전체가 프레임안에 들어오도록 하세요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "당신의 사진이 아이디에 있는 사진과 동일인물 입니까?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "공통 질문들" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "왜 내 사진이 필요한가요?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "확인 과정의 하나로 신분 확인하기 위해 당신의 사진이 필요합니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "이 사진으로 무엇을 하실것인가요?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "당신의 신분을 확인하는데만 사용합니다. 다른 곳에는 표시되지 않습니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "당신의 사진이 좋다고 확인하면 단계 2로 갈 수 있습니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "당신의 아이디를 보여주세요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "자신의 아이디 사진을 촬영하기 위해 웹캠을 사용하십시요. 그러면 당신 계정에 있는 사진 및 이름과 비교할 것입니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "아이디가 잘 조명이 되도록 하십시요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr " 반짝이는 부분이 없게 하십시요" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "당신의 사진을 볼 수 있고, 이름을 읽을수 있도록 하십시요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "중요한 정보가 가려지지 않도록 손을 모서리 부분에 있도록 하십시요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "왜 내 아이디의 사진이 필요하나요?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "신분 확인하기 위해 당신의 아이디에서 사진 및 이름을 맞추어볼 필요가 있습니다 " - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"우리는 결과물을 암호화하고 검토를 위해 보안 인증 서비스에 전달합니다. 최고 수준의 보안으로 사진이나 정보를 저장하여 다른 곳에 유출하지" -" 않습니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "ID가 검토완료 되었으면, 3단계로 가십시오." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "3단계 : 정보 확인" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "과제 제출물 검토" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "아래의 사진과 정보를 검토 해주십시오." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "사진을 확인하고 아래의 요청사항과 맞는지 검토하세요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "이 사진은 아래의 요청사항과 맞춰볼 필요가 있습니다:" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "정상" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "전체 얼굴 표시" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "ID의 사진과 얼굴이 일치해야합니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "가시적인가 (너무 멀리 떨어져있거나 반사되지 않았나)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "ID의 이름과 아래의 계좌의 이름과 일치해야 합니다." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "사진이 아래의 요청사항과 맞지 않습니까?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "사진을 다시 찍으세요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "이름을 확인하세요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "이름을 편집하세요." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "모든 사항이 일치하셨습니다." - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "" @@ -17291,11 +16985,11 @@ msgid "New Course" msgstr "" #: cms/templates/index.html -msgid "New Library" +msgid "Email staff to create course" msgstr "" #: cms/templates/index.html -msgid "Email staff to create course" +msgid "New Library" msgstr "" #: cms/templates/index.html @@ -17920,8 +17614,6 @@ msgstr "" msgid "The nuts and bolts of your course" msgstr "" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "" @@ -17981,7 +17673,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -18701,6 +18403,10 @@ msgstr "" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "키 문자열입니다." diff --git a/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo b/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo index 4451d39861..73b3c53164 100644 Binary files a/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo and b/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/ko_KR/LC_MESSAGES/djangojs.po b/conf/locale/ko_KR/LC_MESSAGES/djangojs.po index 6c9b544f31..8ae989112f 100644 --- a/conf/locale/ko_KR/LC_MESSAGES/djangojs.po +++ b/conf/locale/ko_KR/LC_MESSAGES/djangojs.po @@ -47,8 +47,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-26 19:41+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/edx-platform/language/ko_KR/)\n" "MIME-Version: 1.0\n" @@ -97,11 +97,6 @@ msgstr "" msgid "Cancel" msgstr "취소" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -236,6 +231,10 @@ msgid "(%(num_points)s point possible)" msgid_plural "(%(num_points)s points possible)" msgstr[0] "" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "" @@ -3605,6 +3604,18 @@ msgstr "" msgid "Could not retrieve payment information" msgstr "" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "확인" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -3780,6 +3791,10 @@ msgstr "" msgid "OpenAssessment Save Error" msgstr "" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "" @@ -4047,6 +4062,9 @@ msgid "" msgstr "" #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "" @@ -5147,6 +5165,45 @@ msgstr "" msgid "Forgot password?" msgstr "" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "로그인" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5173,12 +5230,12 @@ msgid "An error occurred when signing you in to %(platformName)s." msgstr "" #: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "로그인" +msgid "or sign in with" +msgstr "" #: lms/templates/student_account/login.underscore -msgid "or sign in with" +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" msgstr "" #: lms/templates/student_account/login.underscore @@ -5585,6 +5642,22 @@ msgid "" "photo." msgstr "" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "" @@ -5644,10 +5717,6 @@ msgstr "" msgid "Before proceeding, please confirm that your details match" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "확인" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/pt_BR/LC_MESSAGES/django.mo b/conf/locale/pt_BR/LC_MESSAGES/django.mo index a689ad1059..17449400b6 100644 Binary files a/conf/locale/pt_BR/LC_MESSAGES/django.mo and b/conf/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/conf/locale/pt_BR/LC_MESSAGES/django.po b/conf/locale/pt_BR/LC_MESSAGES/django.po index 95e7dd1672..b70462eba7 100644 --- a/conf/locale/pt_BR/LC_MESSAGES/django.po +++ b/conf/locale/pt_BR/LC_MESSAGES/django.po @@ -38,6 +38,7 @@ # Marcelo Soares Souza , 2014 # Marco Túlio Pires , 2014 # Mariana Baroni , 2014 +# Mariana Moretti , 2015 # Matheus Andrade , 2014 # snipernael , 2015 # niels006 , 2014 @@ -140,6 +141,7 @@ # RODRIGOR77 , 2014 # danielnora , 2014 # Sarina Canelake , 2014 +# Thais de Assis Angeloni , 2015 # Thiago Perrotta , 2014 # Thiago Vieira , 2014 # Tulio Simoes Martins Padilha , 2014 @@ -219,8 +221,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" -"PO-Revision-Date: 2015-05-28 20:00+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" +"PO-Revision-Date: 2015-06-29 04:46+0000\n" "Last-Translator: javiercencig \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/edx-platform/language/pt_BR/)\n" "MIME-Version: 1.0\n" @@ -729,13 +731,8 @@ msgid "Too many failed login attempts. Try again later." msgstr "Muitas tentativas de acesso sem sucesso. Tente novamente mais tarde." #: common/djangoapps/student/views.py lms/templates/provider_login.html -#, fuzzy msgid "Email or password is incorrect." -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Endereço de e-mail ou senha incorretos.\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"E-mail ou senha incorretos." +msgstr "Endereço de e-mail ou senha incorretos." #: common/djangoapps/student/views.py msgid "" @@ -842,13 +839,28 @@ msgstr "" "Não foi possível enviar o e-mail com o link de ativação. Por favor, tente " "novamente mais tarde." -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "Nome (obrigatório)" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "ID inválido" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1296,6 +1308,24 @@ msgstr "não respondida" msgid "processing" msgstr "processando" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -2053,13 +2083,8 @@ msgstr "Em que etapa da tarefa atual o aluno se encontra." #: common/lib/xmodule/xmodule/combined_open_ended_module.py #: common/lib/xmodule/xmodule/peer_grading_module.py #: lms/templates/peer_grading/peer_grading.html -#, fuzzy msgid "Graded" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Corrigida\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Avaliado" +msgstr "Corrigida" #: common/lib/xmodule/xmodule/combined_open_ended_module.py msgid "" @@ -2178,6 +2203,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "A ser definida" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "Passaportes LTI" @@ -3056,13 +3091,6 @@ msgstr "" msgid "General" msgstr "Geral" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "A ser definida" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "" @@ -4550,13 +4578,8 @@ msgstr "Pesquisar" #: common/static/js/vendor/mathjax-MathJax-c9db6ac/docs/source/mjtheme/layout.html #: lms/templates/static_templates/copyright.html -#, fuzzy msgid "Copyright" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Direitos autorais\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Direito autoral" +msgstr "Direitos autorais" #: lms/djangoapps/branding/api.py msgid "" @@ -4944,13 +4967,8 @@ msgstr "Nota" #: lms/djangoapps/class_dashboard/dashboard_data.py #: lms/templates/instructor/instructor_dashboard_2/metrics.html -#, fuzzy msgid "Percent" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Por cento\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Percentual" +msgstr "Por cento" #: lms/djangoapps/class_dashboard/dashboard_data.py msgid "Opened by this number of students" @@ -5700,13 +5718,8 @@ msgstr "Sexo" #: lms/djangoapps/instructor/views/api.py #: lms/djangoapps/instructor_task/tasks_helper.py #: lms/templates/instructor/instructor_dashboard_2/instructor_analytics.html -#, fuzzy msgid "Level of Education" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Nível educacional\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Nível de formação" +msgstr "Nível educacional" #: lms/djangoapps/instructor/views/api.py #: lms/djangoapps/instructor_task/tasks_helper.py lms/templates/register.html @@ -6161,7 +6174,6 @@ msgstr "ID" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "Nome completo" @@ -6681,13 +6693,8 @@ msgstr "Tarefas foram marcadas para revisão" #: lms/djangoapps/open_ended_grading/views.py #: lms/templates/instructor/staff_grading.html -#, fuzzy msgid "Staff grading" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Correção pela equipe\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Avaliação pela equipe" +msgstr "Correção pela equipe" #. Translators: "Peer grading" appears on a tab that allows #. students to view open-ended problems that require grading @@ -7026,7 +7033,6 @@ msgstr "" "É preciso realizar o acesso para adicionar itens ao carrinho de compras" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "O curso solicitado não existe." @@ -7691,22 +7697,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "Baixar relatórios em CSV" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -#, fuzzy -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Ocorreu um erro ao informar a data. Ela deve ter o formato AAAA-MM-DD\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Ocorreu um erro ao informar a data. Siga o formato AAAA-MM-DD" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -7813,9 +7803,9 @@ msgstr "" msgid "Payment confirmation" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/djangoapps/verify_student/views.py msgid "Take photo" -msgstr "Tirar foto" +msgstr "" #: lms/djangoapps/verify_student/views.py msgid "Take a photo of your ID" @@ -8131,13 +8121,8 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/generate_registarion_codes_modal.html #: lms/templates/instructor/instructor_dashboard_2/invalidate_registration_code_modal.html #: lms/templates/instructor/instructor_dashboard_2/set_course_mode_price_modal.html -#, fuzzy msgid "Required Information" -msgstr "" -"#-#-#-#-# django-partial.po (edx-platform) #-#-#-#-#\n" -"Informação obrigatória\n" -"#-#-#-#-# mako.po (edx-platform) #-#-#-#-#\n" -"Informação necessária" +msgstr "Informação obrigatória" #: lms/templates/registration/password_reset_confirm.html msgid "Your New Password" @@ -9019,6 +9004,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -9275,7 +9268,6 @@ msgid "Loading" msgstr "" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "fechar" @@ -9416,11 +9408,6 @@ msgstr "Ajuda" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -9449,10 +9436,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -9461,6 +9444,10 @@ msgstr "" msgid "Sorry, there was an error when trying to enroll you" msgstr "" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -12280,16 +12267,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -12305,8 +12296,7 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" +msgid "Less" msgstr "" #: lms/templates/courseware/progress.html @@ -12600,6 +12590,37 @@ msgid "" "{unenroll_link_start}unenroll{unenroll_link_end} from this course" msgstr "" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "editar" @@ -13080,6 +13101,10 @@ msgstr "" msgid "Open" msgstr "" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -15800,10 +15825,19 @@ msgstr "" msgid "Payment" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "Baixar relatórios em CSV" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "Baixar dados em CSV" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "Ocorreu um erro ao informar a data. Siga o formato AAAA-MM-DD" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "Estes relatórios são delimitados por datas de início e término." @@ -16121,14 +16155,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "Revisar" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "Confirmação" @@ -16309,105 +16339,14 @@ msgid "" " href=\"{mail_to_link}\"\">{mail_to_link}." msgstr "" -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "Edite o seu nome" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "O seguinte erro ocorreu ao editar o seu nome: " - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"Para manter a credibilidade dos certificados emitidos por {platform}, todas " -"as alterações de nomes serão registradas e gravadas. " - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "Motivo para a alteração do nome: " - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "Alterar o meu nome" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "Está tendo algum problema técnico?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "Alguma dúvida?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"Por favor, leia a {a_start}nossa seção de perguntas frequentes para ver as " -"dúvidas mais comuns sobre os nossos certificados{a_end}." - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "Requisitos técnicos" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "Editar o seu nome completo" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "O seguinte erro ocorreu ao editar o seu nome: " + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -16438,385 +16377,53 @@ msgstr "" msgid "Enroll In {course_name}" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "Alguma dúvida?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"Por favor, leia a {a_start}nossa seção de perguntas frequentes para ver as " +"dúvidas mais comuns sobre os nossos certificados{a_end}." + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "Requisitos técnicos" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "Nova verificação" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "Webcam não encontrada" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." -msgstr "" -"A sua webcam não parece estar conectada ao computador. Verifique novamente " -"se a webcam está conectada e funcionando para poder continuar." - -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "Flash não encontrado" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"Parece que você não tem o Flash instalado. {a_start} Baixe o Flash {a_end} " -"para continuar a sua inscrição." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "Erro ao enviar as suas imagens" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"Algo deu errado. Por favor, confirme os seu detalhes e tente novamente." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "O seu progresso" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "Passo atual:" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "Tire a sua foto novamente" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "Tire novamente a foto da sua identidade" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "Tire novamente a sua foto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"Utilize a webcam para tirar uma foto do seu rosto para que possamos compará-" -"la à foto do seu documento de identificação." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"Não vê sua fotografia? Certifique-se de permitir que o seu navegador utilize" -" a câmera quando ele pedir permissão." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "Tirar foto novamente" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "Ótimo" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "Dicas para tirar uma boa foto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "Certifique-se que o seu rosto esteja bem iluminado" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "Certifique-se que o seu rosto esteja dentro dos limites da borda" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "" -"Podemos comparar a foto que você tirou com a do seu documento de " -"identificação?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "Dúvidas comuns" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "Por que a minha foto é necessária?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" -"Como parte do processo de verificação, precisamos da sua foto para confirmar" -" a sua identidade." - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "Como esta foto será utilizada?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" -"Utilizaremos a sua foto apenas para verificar a sua identidade. Ela não será" -" exibida em nenhum outro local." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "" -"Assim que você confirmar que sua foto está boa, você pode prosseguir para o " -"segundo passo." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "Vá até o passo 2: tire novamente a foto da sua identidade" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "Mostre-nos o seu documento de identificação" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"Utilize a webcam para tirar uma foto do seu documento de identificação para " -"que possamos compará-la à foto e ao nome na sua conta. " - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "Certifique-se que o documento esteja bem iluminado" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "Certifique-se que não haja nenhum brilho" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "" -"Certifique-se que tanto a foto como o nome estejam nítidos e identificáveis" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" -"Tente manter os seus dedos nas bordas para não encobrir informações " -"importantes" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "Por que é necessária a foto do meu documento de identificação?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" -"Precisamos comparar o seu documento de identidade com a sua foto e nome para" -" confirmar a sua identidade." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"Ele será criptografado e enviado para o nosso serviço seguro de autorização." -" Aplicamos os mais altos níveis de segurança e não guardamos quaisquer fotos" -" ou informações em lugar algum após a conclusão da verificação." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" -"Assim que estiver satisfeito com a foto do seu documento, você pode passar " -"para o passo 3." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "Vá para o Passo 3: revisar as suas informações" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "Verificar o seu envio" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" -"Certifique-se que podemos verificar a sua identidade com as fotos e " -"informações abaixo." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "Revise as fotos tiradas" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" -"Por favor, revise as fotos e verifique se elas satisfazem os requisitos " -"abaixo." - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "A foto acima deve satisfazer os seguintes requisitos:" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "Estar bem iluminada" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "Mostrar todo o seu rosto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "" -"A foto no seu documento de identidade deve corresponder à foto do seu rosto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "Ser legível (não muito distante, sem brilho intenso)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "" -"O nome no seu documento de identidade deve corresponder ao da conta abaixo" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "As fotos não satisfazem os requisitos?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "Tirar outra foto" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "Verifique o seu nome" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "Edite o seu nome" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"Após verificar que os detalhes fornecidos atendem aos requisitos, confirme o" -" envio da sua nova verificação." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "Sim! Todos os detalhes coincidem." - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "Por favor, faça uma nova verificação para continuar" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"Para continuar no curso {course} com verificação de identidade, é necessário" -" verificar novamente a sua identidade até {date}. Ir para o URL " -"correspondente." - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "Confirmação de envio da nova verificação " - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "As suas credenciais foram atualizadas" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"Nós obtivemos as informações submetidas novamente e em breve iremos analisá-" -"las para verificar a sua identidade. Você deve receber uma atualização em " -"relação ao seu estado de verificação dentro de 1-2 dias. Nesse período você " -"ainda terá acesso a todo o conteúdo do curso." - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "Retornar ao painel de controle" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "Ocorreu um erro durante a nova verificação " - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"A sua nova verificação foi envia após o prazo e você não pode mais voltar a " -"ser verificado." - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" -"Por favor, entre em contato com o suporte caso considere que esta mensagem " -"foi enviada por engano." - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(ativo){span_end}" @@ -18325,11 +17932,11 @@ msgid "New Course" msgstr "" #: cms/templates/index.html -msgid "New Library" +msgid "Email staff to create course" msgstr "" #: cms/templates/index.html -msgid "Email staff to create course" +msgid "New Library" msgstr "" #: cms/templates/index.html @@ -18954,8 +18561,6 @@ msgstr "" msgid "The nuts and bolts of your course" msgstr "" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "" @@ -19015,7 +18620,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -19737,6 +19352,10 @@ msgstr "" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "Esta é uma sequência chave." diff --git a/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo b/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo index a8ec3b15a4..3b1cba39cf 100644 Binary files a/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo and b/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/pt_BR/LC_MESSAGES/djangojs.po b/conf/locale/pt_BR/LC_MESSAGES/djangojs.po index d64ca62e62..98b169b137 100644 --- a/conf/locale/pt_BR/LC_MESSAGES/djangojs.po +++ b/conf/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -70,6 +70,7 @@ # Cleomir Waiczyk , 2015 # Daniel Linhares , 2014 # Deusilene Sousa Matos , 2015 +# Diego Rabatone Oliveira , 2015 # Edgar Aparecido Pereira de Melo , 2014 # aivuk , 2014 # Fernando Nunes , 2015 @@ -144,9 +145,9 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" -"Last-Translator: Sarina Canelake \n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-29 04:49+0000\n" +"Last-Translator: Fernando Nunes \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/edx-platform/language/pt_BR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -195,10 +196,6 @@ msgstr "OK" msgid "Cancel" msgstr "Cancelar" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -msgid "This link will open in a new browser window/tab" -msgstr "Este link será aberto em uma nova janela/guia do navegador" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -310,6 +307,10 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "(%(num_points) ponto possível)" msgstr[1] "(%(num_points)s pontos possíveis)" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "Resposta:" @@ -3803,6 +3804,19 @@ msgstr "" msgid "Could not retrieve payment information" msgstr "" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -3981,6 +3995,10 @@ msgstr "" msgid "OpenAssessment Save Error" msgstr "" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "Este link irá abrir em uma janela modal" @@ -4248,6 +4266,9 @@ msgid "" msgstr "" #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "" @@ -5359,6 +5380,45 @@ msgstr "" msgid "Forgot password?" msgstr "" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5385,12 +5445,12 @@ msgid "An error occurred when signing you in to %(platformName)s." msgstr "" #: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" +msgid "or sign in with" msgstr "" #: lms/templates/student_account/login.underscore -msgid "or sign in with" +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" msgstr "" #: lms/templates/student_account/login.underscore @@ -5797,6 +5857,22 @@ msgid "" "photo." msgstr "" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "" @@ -5856,10 +5932,6 @@ msgstr "" msgid "Before proceeding, please confirm that your details match" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/rtl/LC_MESSAGES/django.mo b/conf/locale/rtl/LC_MESSAGES/django.mo index c9c719a0c6..37f2f3418e 100644 Binary files a/conf/locale/rtl/LC_MESSAGES/django.mo and b/conf/locale/rtl/LC_MESSAGES/django.mo differ diff --git a/conf/locale/rtl/LC_MESSAGES/django.po b/conf/locale/rtl/LC_MESSAGES/django.po index 1627618218..3997ea21e8 100644 --- a/conf/locale/rtl/LC_MESSAGES/django.po +++ b/conf/locale/rtl/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: 2015-06-19 13:47+0000\n" -"PO-Revision-Date: 2015-06-19 13:47:59.148161\n" +"POT-Creation-Date: 2015-06-29 12:36+0000\n" +"PO-Revision-Date: 2015-06-29 12:36:50.076432\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -693,13 +693,35 @@ msgstr "شر شذذخعرف صهفا فاهس ث-وشهم شمقثشيغ ثطه msgid "Unable to send email activation link. Please try again later." msgstr "عرشزمث فخ سثري ثوشهم شذفهدشفهخر مهرن. حمثشسث فقغ شلشهر مشفثق." -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "رشوث قثضعهقثي" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "شعفاثرفهذشفهخر صهفا {} هس ذعققثرفمغ عرشدشهمشزمث." -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "هردشمهي هي" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" +"سثذخريشقغ حقخدهيثقس شقث يهسحمشغثي مثسس حقخوهرثرفمغ, هر ش سثحشقشفث مهسف خب " +"\"هرسفهفعفهخر\" مخلهر حقخدهيثقس." + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" +"هب فاهس خحفهخر هس ثرشزمثي, عسثقس صهمم رخف زث شسنثي فخ ذخربهقو فاثهق يثفشهمس " +"(رشوث, ثوشهم, ثفذ.) يعقهرل فاث قثلهسفقشفهخر حقخذثسس. خرمغ سثمثذف فاهس خحفهخر" +" بخق فقعسفثي حقخدهيثقس فاشف شقث نرخصر فخ حقخدهيث شذذعقشفث عسثق هربخقوشفهخر." + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" +"هب فاهس خحفهخر هس سثمثذفثي, عسثقس صهمم رخف زث قثضعهقثي فخ ذخربهقو فاثهق " +"ثوشهم, شري فاثهق شذذخعرف صهمم زث شذفهدشفثي هووثيهشفثمغ عحخر قثلهسفقشفهخر." #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1148,6 +1170,24 @@ msgstr "عرشرسصثقثي" msgid "processing" msgstr "حقخذثسسهرل" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "فاهس هس ذخققثذف." + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "فاهس هس هرذخققثذف." + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "فاهس هس عرشرسصثقثي." + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "فاهس هس زثهرل حقخذثسسثي." + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -2018,6 +2058,16 @@ msgstr "" "خقش1 هس رخ مخرلثق سعححخقفثي. فخ عسث فاهس شسسثسسوثرف, قثحمشذث فاهس خقش1 " "ذخوحخرثرف صهفا شر خقش2 ذخوحخرثرف." +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "فزي" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "مفه حشسسحخقفس" @@ -3015,13 +3065,6 @@ msgstr "" msgid "General" msgstr "لثرثقشم" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "فزي" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "يهسذعسسهخر هي" @@ -6276,7 +6319,6 @@ msgstr "هي" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "بعمم رشوث" @@ -7137,6 +7179,7 @@ msgid "Verified Enrollment" msgstr "دثقهبهثي ثرقخمموثرف" #: lms/djangoapps/shoppingcart/reports.py +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue" msgstr "لقخسس قثدثرعث" @@ -7177,7 +7220,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "غخع وعسف زث مخللثي-هر فخ شيي فخ ش ساخححهرل ذشقف" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "فاث ذخعقسث غخع قثضعثسفثي يخثس رخف ثطهسف." @@ -7979,18 +8021,6 @@ msgstr "" " حخسسهزمث شذفهخر: قثفقغ صهفا ش يهببثقثرف بخقو خب حشغوثرف.\n" " " -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "يخصرمخشي ذسد قثحخقفس" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "" -"فاثقث صشس شر ثققخق هر غخعق يشفث هرحعف. هف ساخعمي زث بخقوشففثي شس غغغغ-وو-يي" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "رخ عسثق صهفا فاث حقخدهيثي ثوشهم شييقثسس ثطهسفس." @@ -8096,7 +8126,6 @@ msgid "Payment confirmation" msgstr "حشغوثرف ذخربهقوشفهخر" #: lms/djangoapps/verify_student/views.py -#: lms/templates/verify_student/photo_reverification.html msgid "Take photo" msgstr "فشنث حاخفخ" @@ -9346,6 +9375,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "هردشمهي حقثقثضعهسهفث ذخعقسث نثغ" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "شر ثققخق خذذعققثي صاهمث فقغهرل فخ سشدث غخعق فشزس" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "فشزس ثطذثحفهخر" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "فاهس لقخعح ذخربهلعقشفهخر هس هر عسث شري ذشررخف زث يثمثفثي." @@ -9614,7 +9651,6 @@ msgid "Loading" msgstr "مخشيهرل" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "ذمخسث" @@ -9754,11 +9790,6 @@ msgstr "اثمح" msgid "Sign Out" msgstr "سهلر خعف" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "رشوث" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "شمم قهلافس قثسثقدثي" @@ -9787,10 +9818,6 @@ msgstr "ذقثشفهدث ذخووخرس مهذثرسثي ذخرفثرف, صهف msgid "Some Rights Reserved" msgstr "سخوث قهلافس قثسثقدثي" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "عحلقشيث غخعق ثرقخمموثرف بخق {} | ذاخخسث غخعق فقشذن" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "ثرقخمم هر {} | ذاخخسث غخعق فقشذن" @@ -9799,6 +9826,10 @@ msgstr "ثرقخمم هر {} | ذاخخسث غخعق فقشذن" msgid "Sorry, there was an error when trying to enroll you" msgstr "سخققغ, فاثقث صشس شر ثققخق صاثر فقغهرل فخ ثرقخمم غخع" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "ذخرلقشفعمشفهخرس! غخع شقث رخص ثرقخممثي هر {course_name}" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "حعقسعث شذشيثوهذ ذقثيهف صهفا ش دثقهبهثي ذثقفهبهذشفث" @@ -12689,18 +12720,22 @@ msgstr "غخع شقث رخ مخرلثق ثمهلهزمث بخق فاهس ذخع msgid "You have met the requirements for credit in this course." msgstr "غخع اشدث وثف فاث قثضعهقثوثرفس بخق ذقثيهف هر فاهس ذخعقسث." +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "{link} فخ حعقذاشسث ذخعقسث ذقثيهف." + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "لخ فخ غخعق يشسازخشقي" -#: lms/templates/courseware/progress.html -msgid "to purchase course credit." -msgstr "فخ حعقذاشسث ذخعقسث ذقثيهف." - #: lms/templates/courseware/progress.html msgid "You have not yet met the requirements for credit." msgstr "غخع اشدث رخف غثف وثف فاث قثضعهقثوثرفس بخق ذقثيهف." +#: lms/templates/courseware/progress.html +msgid "display_name" +msgstr "يهسحمشغ_رشوث" + #: lms/templates/courseware/progress.html msgid "Verification Submitted" msgstr "دثقهبهذشفهخر سعزوهففثي" @@ -12714,9 +12749,8 @@ msgid "Upcoming" msgstr "عحذخوهرل" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" -msgstr "وخقث" +msgid "Less" +msgstr "مثسس" #: lms/templates/courseware/progress.html msgid "{earned:.3n} of {total:.3n} possible points" @@ -13021,6 +13055,44 @@ msgstr "" "اخميثق{contact_link_end} فخ قثضعثسف حشغوثرف, خق غخع ذشر " "{unenroll_link_start}عرثرقخمم{unenroll_link_end} بقخو فاهس ذخعقسث" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" +"{username}, غخعق ثمهلهزهمهفغ بخق ذقثيهف ثطحهقثس خر {expiry}. يخر'ف وهسس خعف!" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "{congrats} {username}, غخع اشدث وثثف قثضعهقثوثرفس بخق ذقثيهف." + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "حعقذاشسث ذقثيهف" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" +"فاشرن غخع, غخعق حشغوثرف هس ذخوحمثفث, غخعق ذقثيهف هس حقخذثسسهرل. حمثشسث سثث " +"{provider_link} بخق وخقث هربخقوشفهخر." + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" +"فاشرن غخع, غخعق ذقثيهف هس شححقخدثي. حمثشسث سثث {provider_link} بخق وخقث " +"هربخقوشفهخر." + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" +"غخعق ذقثيهف اشس زثثر يثرهثي. حمثشسث ذخرفشذف {provider_link} بخق وخقث " +"هربخقوشفهخر." + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "ثيهف" @@ -13508,6 +13580,10 @@ msgstr "عرحهر" msgid "Open" msgstr "خحثر" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "وخقث" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -16332,10 +16408,20 @@ msgstr "دهثص ذخعقسثس" msgid "Payment" msgstr "حشغوثرف" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "يخصرمخشي ذسد قثحخقفس" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "يخصرمخشي ذسد يشفش" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" +"فاثقث صشس شر ثققخق هر غخعق يشفث هرحعف. هف ساخعمي زث بخقوشففثي شس غغغغ-وو-يي" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "فاثسث قثحخقفس شقث يثمهوهفثي زغ سفشقف شري ثري يشفثس." @@ -16678,14 +16764,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "{platform_name} - ساخححهرل ذشقف" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "قثدهثص" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "ذخربهقوشفهخر" @@ -16878,117 +16960,14 @@ msgstr "" "هب غخع اشدث شرغ ضعثسفهخرس شزخعف فاهس ذخعقسث خق فاهس بخقو, غخع ذشر ذخرفشذف {mail_to_link}." -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "ثيهف غخعق رشوث" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "فاث بخممخصهرل ثققخق خذذعققثي صاهمث ثيهفهرل غخعق رشوث:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" -"فخ عحاخمي فاث ذقثيهزهمهفغ خب {platform} ذثقفهبهذشفثس, شمم رشوث ذاشرلثس صهمم " -"زث مخللثي شري قثذخقيثي." - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "قثشسخر بخق رشوث ذاشرلث:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "ذاشرلث وغ رشوث" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "صاغ يخ ه رثثي فخ قث-دثقهبغ وغ هيثرفهفغ?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" -"غخع وشغ رثثي فخ قث-دثقهبغ غخعق هيثرفهفغ هب شر ثققخق خذذعقس صهفا غخعق " -"دثقهبهذشفهخر خق هب غخعق دثقهبهذشفهخر اشس ثطحهقثي. شمم دثقهبهذشفهخرس ثطحهقث " -"شبفثق خرث غثشق. فاث قث-دثقهبهذشفهخر حقخذثسس هس فاث سشوث شس فاث خقهلهرشم " -"دثقهبهذشفهخر حقخذثسس. غخع رثثي ش صثزذشو شري ش لخدثقروثرف-هسسعثي حاخفخ هي." - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "اشدهرل فثذارهذشم فقخعزمث?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" -"حمثشسث وشنث سعقث غخعق زقخصسثق هس عحيشفثي فخ فاث {a_start}وخسف قثذثرف" -" دثقسهخر حخسسهزمث{a_end}. شمسخ, حمثشسث وشنث سعقث غخعق " -"صثزذشو هس حمعللثي هر, فعقرثي خر, شري شممخصثي فخ بعرذفهخر هر غخعق صثز" -" زقخصسثق (ذخووخرمغ شيتعسفشزمث هر غخعق زقخصسثق سثففهرلس)" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "اشدث ضعثسفهخرس?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" -"حمثشسث قثشي {a_start}خعق بشضس فخ دهثص ذخووخر ضعثسفهخرس شزخعف خعق " -"ذثقفهبهذشفثس{a_end}." - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "غخع شقث عحلقشيهرل غخعق ثرقخمموثرف بخق: {course_name}" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "غخع شقث قث-دثقهبغهرل بخق: {course_name}" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "غخع شقث ثرقخممهرل هر: {course_name}" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "ذخرلقشفعمشفهخرس! غخع شقث رخص ثرقخممثي هر {course_display}" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "فثذارهذشم قثضعهقثوثرفس" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" -"حمثشسث وشنث سعقث غخعق زقخصسثق هس عحيشفثي فخ فاث {a_start}وخسف قثذثرف دثقسهخر" -" حخسسهزمث{a_end}. شمسخ, حمثشسث وشنث سعقث غخعق صثزذشو هس حمعللثي هر, " -"فعقرثي خر, شري شممخصثي فخ بعرذفهخر هر غخعق صثز زقخصسثق (ذخووخرمغ شيتعسفشزمث " -"هر غخعق زقخصسثق سثففهرلس)." - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "ثيهف غخعق بعمم رشوث" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "فاث بخممخصهرل ثققخق خذذعققثي صاهمث ثيهفهرل غخعق رشوث:" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "قث-دثقهبغ بخق {course_name}" @@ -17021,379 +17000,60 @@ msgstr "دثقهبغ بخق {course_name}" msgid "Enroll In {course_name}" msgstr "ثرقخمم هر {course_name}" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "اشدث ضعثسفهخرس?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" +"حمثشسث قثشي {a_start}خعق بشضس فخ دهثص ذخووخر ضعثسفهخرس شزخعف خعق " +"ذثقفهبهذشفثس{a_end}." + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "فثذارهذشم قثضعهقثوثرفس" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" +"حمثشسث وشنث سعقث غخعق زقخصسثق هس عحيشفثي فخ فاث {a_start}وخسف قثذثرف دثقسهخر" +" حخسسهزمث{a_end}. شمسخ, حمثشسث وشنث سعقث غخعق صثزذشو هس حمعللثي هر, " +"فعقرثي خر, شري شممخصثي فخ بعرذفهخر هر غخعق صثز زقخصسثق (ذخووخرمغ شيتعسفشزمث " +"هر غخعق زقخصسثق سثففهرلس)." + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "قث-دثقهبهذشفهخر" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "رخ صثزذشو يثفثذفثي" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "هيثرفهفغ دثقهبهذشفهخر" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -"غخع يخر'ف سثثو فخ اشدث ش صثزذشو ذخررثذفثي. يخعزمث-ذاثذن فاشف غخعق صثزذشو هس " -"ذخررثذفثي شري صخقنهرل فخ ذخرفهرعث." +"غخع اشدث شمقثشيغ سعزوهففثي غخعق دثقهبهذشفهخر هربخقوشفهخر. غخع صهمم سثث ش " +"وثسسشلث خر غخعق يشسازخشقي صاثر فاث دثقهبهذشفهخر حقخذثسس هس ذخوحمثفث (عسعشممغ" +" صهفاهر 1-2 يشغس)." -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "رخ بمشسا يثفثذفثي" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." +msgstr "غخع ذشررخف دثقهبغ غخعق هيثرفهفغ شف فاهس فهوث." -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "" -"غخع يخر'ف سثثو فخ اشدث بمشسا هرسفشممثي. {a_start} لثف بمشسا {a_end} فخ " -"ذخرفهرعث غخعق قثلهسفقشفهخر." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "ثققخق سعزوهففهرل غخعق هوشلثس" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" -"خخحس! سخوثفاهرل صثرف صقخرل. حمثشسث ذخربهقو غخعق يثفشهمس شري فقغ شلشهر." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "دثقهبغ غخعق هيثرفهفغ" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." -msgstr "" -"فخ دثقهبغ غخعق هيثرفهفغ شري ذخرفهرعث شس ش دثقهبهثي سفعيثرف هر فاهس ذخعقسث, " -"ذخوحمثفث فاث بخممخصهرل سفثحس {start_bold}زثبخقث فاث ذخعقسث دثقهبهذشفهخر " -"يثشيمهرث{end_bold}. هب غخع يخ رخف دثقهبغ غخعق هيثرفهفغ, غخع ذشر سفهمم " -"قثذثهدث شر اخرخق ذخيث ذثقفهبهذشفث بخق فاث ذخعقسث." - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "غخعق حقخلقثسس" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "ذعققثرف سفثح: " - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "قث-فشنث حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "قث-فشنث هي حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "قث-فشنث غخعق حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" -"عسث غخعق صثزذشو فخ فشنث ش حهذفعقث خب غخعق بشذث سخ صث ذشر وشفذا هف صهفا فاث " -"حهذفعقث خر غخعق هي." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" -"يخر'ف سثث غخعق حهذفعقث? وشنث سعقث فخ شممخص غخعق زقخصسثق فخ عسث غخعق ذشوثقش " -"صاثر هف شسنس بخق حثقوهسسهخر." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "قثفشنث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "مخخنس لخخي" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "فهحس خر فشنهرل ش سعذذثسسبعم حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "وشنث سعقث غخعق بشذث هس صثمم-مهف" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "زث سعقث غخعق ثرفهقث بشذث هس هرسهيث فاث بقشوث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "ذشر صث وشفذا فاث حاخفخ غخع فخخن صهفا فاث خرث خر غخعق هي?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" -"خرذث هر حخسهفهخر, عسث فاث ذشوثقش زعففخر {btn_icon} فخ ذشحفعقث غخعق حهذفعقث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "عسث فاث ذاثذنوشقن زعففخر {btn_icon} خرذث غخع شقث اشححغ صهفا فاث حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "ذخووخر ضعثسفهخرس" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "صاغ يخ غخع رثثي وغ حاخفخ?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" -"شس حشقف خب فاث دثقهبهذشفهخر حقخذثسس, صث رثثي غخعق حاخفخ فخ ذخربهقو فاشف غخع " -"شقث غخع." - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "صاشف يخ غخع يخ صهفا فاهس حهذفعقث?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" -"صث خرمغ عسث هف فخ دثقهبغ غخعق هيثرفهفغ. هف هس رخف يهسحمشغثي شرغصاثقث." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "دثقهبهذشفهخر" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "خرذث غخع دثقهبغ غخعق حاخفخ مخخنس لخخي, غخع ذشر وخدث خر فخ سفثح 2." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "لخ فخ سفثح 2: قث-فشنث هي حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "ساخص عس غخعق هي" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" -"عسث غخعق صثزذشو فخ فشنث ش حهذفعقث خب غخعق هي سخ صث ذشر وشفذا هف صهفا غخعق " -"حاخفخ شري فاث رشوث خر غخعق شذذخعرف." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "وشنث سعقث غخعق هي هس صثمم-مهف" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" -"شذذثحفشزمث هيس هرذمعيث يقهدثق'س مهذثرسثس, حشسسحخقفس, خق خفاثق لخدثقوثرف-" -"هسسعثي هيس فاشف هرذمعيث غخعق رشوث شري حاخفخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "ذاثذن فاشف فاثقث هسر'ف شرغ لمشقث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "ثرسعقث فاشف غخع ذشر سثث غخعق حاخفخ شري قثشي غخعق رشوث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" -"فقغ فخ نثثح غخعق بهرلثقس شف فاث ثيلث فخ شدخهي ذخدثقهرل هوحخقفشرف هربخقوشفهخر" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "خرذث هر حخسهفهخر, عسث فاث ذشوثقش زعففخر {btn_icon} فخ ذشحفعقث غخعق هي" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "صاغ يخ غخع رثثي ش حاخفخ خب وغ هي?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" -"صث رثثي فخ وشفذا غخعق هي صهفا غخعق حاخفخ شري رشوث فخ ذخربهقو فاشف غخع شقث " -"غخع." - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" -"صث ثرذقغحف هف شري سثري هف فخ خعق سثذعقث شعفاخقهظشفهخر سثقدهذث بخق قثدهثص. صث" -" عسث فاث اهلاثسف مثدثمس خب سثذعقهفغ شري يخ رخف سشدث فاث حاخفخ خق هربخقوشفهخر" -" شرغصاثقث خرذث فاث وشفذا اشس زثثر ذخوحمثفثي." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "خرذث غخع دثقهبغ غخعق هي حاخفخ مخخنس لخخي, غخع ذشر وخدث خر فخ سفثح 3." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "لخ فخ سفثح 3: قثدهثص غخعق هربخ" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "دثقهبغ غخعق سعزوهسسهخر" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" -"وشنث سعقث صث ذشر دثقهبغ غخعق هيثرفهفغ صهفا فاث حاخفخس شري هربخقوشفهخر زثمخص." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "قثدهثص فاث حاخفخس غخع'دث قث-فشنثر" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" -"حمثشسث قثدهثص فاث حاخفخس شري دثقهبغ فاشف فاثغ وثثف فاث قثضعهقثوثرفس مهسفثي " -"زثمخص." - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "فاث حاخفخ شزخدث رثثيس فخ وثثف فاث بخممخصهرل قثضعهقثوثرفس:" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "زث صثمم مهف" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "ساخص غخعق صاخمث بشذث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "فاث حاخفخ خر غخعق هي وعسف وشفذا فاث حاخفخ خب غخعق بشذث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "زث قثشيشزمث (رخف فخخ بشق شصشغ, رخ لمشقث)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "فاث رشوث خر غخعق هي وعسف وشفذا فاث رشوث خر غخعق شذذخعرف زثمخص" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "حاخفخس يخر'ف وثثف فاث قثضعهقثوثرفس?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "قثفشنث غخعق حاخفخس" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "ذاثذن غخعق رشوث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" -"وشنث سعقث غخعق بعمم رشوث خر غخعق {platform_name} شذذخعرف ({full_name}) " -"وشفذاثس غخعق هي. صث صهمم شمسخ عسث فاهس شس فاث رشوث خر غخعق ذثقفهبهذشفث." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "ثيهف غخعق رشوث" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" -"خرذث غخع دثقهبغ غخعق يثفشهمس وشفذا فاث قثضعهقثوثرفس, غخع ذشر وخدث خرفخ فخ " -"ذخربهقو غخعق قث-دثقهبهذشفهخر سعزوهسسسهخر." - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "غثس! وغ يثفشهمس شمم وشفذا." - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "غخع رثثي فخ قث-دثقهبغ فخ ذخرفهرعث" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" -"فخ ذخرفهرعث هر فاث هي دثقهبهثي فقشذن هر {course}, غخع رثثي فخ قث-دثقهبغ غخعق" -" هيثرفهفغ زغ {date}. لخ فخ عقم." - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "قث-دثقهبهذشفهخر سعزوهسسهخر ذخربهقوشفهخر" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "غخعق ذقثيثرفهشمس اشدث زثثر عحيشفثي" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" -"صث'دث ذشحفعقثي غخعق قث-سعزوهففثي هربخقوشفهخر شري صهمم قثدهثص هف فخ دثقهبغ " -"غخعق هيثرفهفغ ساخقفمغ. غخع ساخعمي قثذثهدث شر عحيشفث فخ غخعق دثقههذشفهخر " -"سفشفعس صهفاهر 1-2 يشغس. هر فاث وثشرفهوث, غخع سفهمم اشدث شذذثسس فخ شمم خب " -"غخعق ذخعقسث ذخرفثرف." - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "قثفعقر فخ غخعق يشسازخشقي" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "قث-دثقهبهذشفهخر بشهمثي" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" -"غخعق قث-دثقهبهذشفهخر صشس سعزوهففثي شبفثق فاث قث-دثقهبهذشفهخر يثشيمهرث, شري " -"غخع ذشر رخ مخرلثق زث قث-دثقهبهثي." - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "حمثشسث ذخرفشذف سعححخقف هب غخع زثمهثدث فاهس وثسسشلث فخ زث هر ثققخق." - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(شذفهدث){span_end}" @@ -19170,14 +18830,14 @@ msgstr "{studio_name} اخوث" msgid "New Course" msgstr "رثص ذخعقسث" -#: cms/templates/index.html -msgid "New Library" -msgstr "رثص مهزقشقغ" - #: cms/templates/index.html msgid "Email staff to create course" msgstr "ثوشهم سفشبب فخ ذقثشفث ذخعقسث" +#: cms/templates/index.html +msgid "New Library" +msgstr "رثص مهزقشقغ" + #: cms/templates/index.html msgid "Please correct the highlighted fields below." msgstr "حمثشسث ذخققثذف فاث اهلامهلافثي بهثميس زثمخص." @@ -19907,8 +19567,6 @@ msgstr "زشسهذ هربخقوشفهخر" msgid "The nuts and bolts of your course" msgstr "فاث رعفس شري زخمفس خب غخعق ذخعقسث" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "فاهس بهثمي هس يهسشزمثي: فاهس هربخقوشفهخر ذشررخف زث ذاشرلثي." @@ -19974,8 +19632,18 @@ msgid "Successful Proctored Exam" msgstr "سعذذثسسبعم حقخذفخقثي ثطشو" #: cms/templates/settings.html -msgid "Successful In Course Reverification" -msgstr "سعذذثسسبعم هر ذخعقسث قثدثقهبهذشفهخر" +msgid "Proctored Exam {number}" +msgstr "حقخذفخقثي ثطشو {number}" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "سعذذثسسبعم هر-ذخعقسث قثدثقهبهذشفهخر" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" +msgstr "هر-ذخعقسث قثدثقهبهذشفهخر {number}" #: cms/templates/settings.html msgid "Access to {display_name}" @@ -20797,6 +20465,10 @@ msgstr "اخص فخ عسث {studio_name} فخ زعهمي غخعق ذخعقسث" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "اشدث حقخزمثوس, ضعثسفهخرس, خق سعللثسفهخرس شزخعف {studio_name}?" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "رشوث" + # empty msgid "This is a key string." msgstr "فاهس هس ش نثغ سفقهرل." diff --git a/conf/locale/rtl/LC_MESSAGES/djangojs.mo b/conf/locale/rtl/LC_MESSAGES/djangojs.mo index 9db45a4ea2..1b4bf396cb 100644 Binary files a/conf/locale/rtl/LC_MESSAGES/djangojs.mo and b/conf/locale/rtl/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/rtl/LC_MESSAGES/djangojs.po b/conf/locale/rtl/LC_MESSAGES/djangojs.po index 8be6fb17ce..adf17a44fb 100644 --- a/conf/locale/rtl/LC_MESSAGES/djangojs.po +++ b/conf/locale/rtl/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: 2015-06-19 13:46+0000\n" -"PO-Revision-Date: 2015-06-19 13:47:59.519977\n" +"POT-Creation-Date: 2015-06-29 12:35+0000\n" +"PO-Revision-Date: 2015-06-29 12:36:50.361468\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -89,11 +89,6 @@ msgstr "خن" msgid "Cancel" msgstr "ذشرذثم" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "فاهس مهرن صهمم خحثر هر ش رثص زقخصسثق صهريخص/فشز" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -235,6 +230,11 @@ msgid_plural "(%(num_points)s points possible)" msgstr[0] "(%(num_points)s حخهرف حخسسهزمث)" msgstr[1] "(%(num_points)s حخهرفس حخسسهزمث)" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" +"فاث لقشيهرل حقخذثسس هس سفهمم قعررهرل. قثبقثسا فاث حشلث فخ سثث عحيشفثس." + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "شرسصثق:" @@ -3806,6 +3806,19 @@ msgstr "ذخعمي رخف سعزوهف خقيثق" msgid "Could not retrieve payment information" msgstr "ذخعمي رخف قثفقهثدث حشغوثرف هربخقوشفهخر" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "فشنث ش حاخفخ خب غخعق هي" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "قثدهثص غخعق هربخ" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "ذخربهقو" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "شر ثققخق اشس خذذعققثي. حمثشسث فقغ قثمخشيهرل فاث حشلث." @@ -3995,6 +4008,10 @@ msgstr "يثمثفهرل" msgid "OpenAssessment Save Error" msgstr "خحثرشسسثسسوثرف سشدث ثققخق" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "فاهس مهرن صهمم خحثر هر ش رثص زقخصسثق صهريخص/فشز" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "فاهس مهرن صهمم خحثر هر ش وخيشم صهريخص" @@ -4279,6 +4296,9 @@ msgstr "" "<%= fileExtensions %> فخ عحمخشي." #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "خق" @@ -5449,6 +5469,45 @@ msgstr "شر ثققخق خذذعققثي. حمثشسث قثمخشي فاث حش msgid "Forgot password?" msgstr "بخقلخف حشسسصخقي?" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "سهلر هر" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "صخعمي غخع مهنث فخ سهلر هر عسهرل غخعق %(providerName)s ذقثيثرفهشمس?" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "سهلر هر عسهرل %(providerName)s" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "ساخص وث خفاثق صشغس فخ سهلر هر خق قثلهسفثق" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "سهلر هر صهفا هرسفهفعفهخر/ذشوحعس ذقثيثرفهشمس" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "ذاخخسث غخعق هرسفهفعفهخر بقخو فاث مهسف زثمخص:" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "زشذن فخ سهلر هر" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "قثلهسفثق صهفا هرسفهفعفهخر/ذشوحعس ذقثيثرفهشمس" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "قثلهسفثق فاقخعلا ثيط" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5479,15 +5538,15 @@ msgstr "صث ذخعمير'ف سهلر غخع هر." msgid "An error occurred when signing you in to %(platformName)s." msgstr "شر ثققخق خذذعققثي صاثر سهلرهرل غخع هر فخ %(platformName)s." -#: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "سهلر هر" - #: lms/templates/student_account/login.underscore msgid "or sign in with" msgstr "خق سهلر هر صهفا" +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" +msgstr "عسث وغ هرسفهفعفهخر/ذشوحعس ذقثيثرفهشمس" + #: lms/templates/student_account/login.underscore msgid "New to %(platformName)s?" msgstr "رثص فخ %(platformName)s?" @@ -5940,6 +5999,26 @@ msgstr "" "ش يقهدثق'س مهذثرسث, حشسسحخقف, خق لخدثقروثرف-هسسعثي هي صهفا غخعق رشوث شري " "حاخفخ." +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "هيثرفهفغ دثقهبهذشفهخر هر حقخلقثسس" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" +"صث اشدث قثذثهدثي غخعق هربخقوشفهخر شري شقث دثقهبغهرل غخعق هيثرفهفغ. غخع صهمم " +"سثث ش وثسسشلث خر غخعق يشسازخشقي صاثر فاث دثقهبهذشفهخر حقخذثسس هس ذخوحمثفث " +"(عسعشممغ صهفاهر 1-2 يشغس). هر فاث وثشرفهوث, غخع ذشر سفهمم شذذثسس شمم " +"شدشهمشزمث ذخعقسث ذخرفثرف." + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "قثفعقر فخ غخعق يشسازخشقي" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "قثدهثص غخعق حاخفخس" @@ -6002,10 +6081,6 @@ msgstr "قثفشنث غخعق حاخفخس" msgid "Before proceeding, please confirm that your details match" msgstr "زثبخقث حقخذثثيهرل, حمثشسث ذخربهقو فاشف غخعق يثفشهمس وشفذا" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "ذخربهقو" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/ru/LC_MESSAGES/django.mo b/conf/locale/ru/LC_MESSAGES/django.mo index fe05effd69..9726042c62 100644 Binary files a/conf/locale/ru/LC_MESSAGES/django.mo and b/conf/locale/ru/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ru/LC_MESSAGES/django.po b/conf/locale/ru/LC_MESSAGES/django.po index 5e349f9e1a..154318aa30 100644 --- a/conf/locale/ru/LC_MESSAGES/django.po +++ b/conf/locale/ru/LC_MESSAGES/django.po @@ -176,7 +176,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" "PO-Revision-Date: 2015-06-01 16:30+0000\n" "Last-Translator: Liubov Fomicheva \n" "Language-Team: Russian (http://www.transifex.com/projects/p/edx-platform/language/ru/)\n" @@ -798,13 +798,28 @@ msgstr "Учётная запись с таким адресом электро msgid "Unable to send email activation link. Please try again later." msgstr "" -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "Имя - обязательное поле" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "Неверный идентификатор" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1252,6 +1267,24 @@ msgstr "" msgid "processing" msgstr "обработка" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -2071,6 +2104,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "" @@ -2918,13 +2961,6 @@ msgstr "" msgid "General" msgstr "" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "" @@ -5860,7 +5896,6 @@ msgstr "" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "Полное имя" @@ -6648,6 +6683,7 @@ msgid "Verified Enrollment" msgstr "" #: lms/djangoapps/shoppingcart/reports.py +#: lms/templates/instructor/instructor_dashboard_2/executive_summary.html msgid "Gross Revenue" msgstr "" @@ -6688,7 +6724,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "Вы должны войти на сайт для того, чтобы добавлять курсы в корзину" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "" @@ -7291,15 +7326,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -msgid "Download CSV Reports" -msgstr "Скачать отчёты в формате CSV" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "Ошибка при вводе даты. Дата должна быть указана в формате ГГГГ-ММ-ДД" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -7403,7 +7429,6 @@ msgid "Payment confirmation" msgstr "" #: lms/djangoapps/verify_student/views.py -#: lms/templates/verify_student/photo_reverification.html msgid "Take photo" msgstr "" @@ -8544,6 +8569,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "" @@ -8800,7 +8833,6 @@ msgid "Loading" msgstr "" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "закрыть" @@ -8942,11 +8974,6 @@ msgstr "Помощь" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -8975,10 +9002,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -8987,6 +9010,10 @@ msgstr "" msgid "Sorry, there was an error when trying to enroll you" msgstr "" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -11694,16 +11721,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -11719,8 +11750,7 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" +msgid "Less" msgstr "" #: lms/templates/courseware/progress.html @@ -11993,6 +12023,37 @@ msgid "" "{unenroll_link_start}unenroll{unenroll_link_end} from this course" msgstr "" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "" @@ -12464,6 +12525,10 @@ msgstr "" msgid "Open" msgstr "" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -14988,10 +15053,19 @@ msgstr "Посмотреть курс" msgid "Payment" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "" @@ -15310,14 +15384,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "Просмотреть" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "Подтверждение" @@ -15490,101 +15560,14 @@ msgstr "" "Если у вас возникнут вопросы относительно курса или этой анкеты, свяжитесь с" " {mail_to_link}." -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "Причина смены имени:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "Изменить моё имя" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "Есть технические проблемы?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "Есть вопросы?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -15615,337 +15598,51 @@ msgstr "" msgid "Enroll In {course_name}" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "Есть вопросы?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "Вебкамера не обнаружена" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" +msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "Flash не обнаружен" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "Текущий шаг: " - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "Убедитесь, что Ваш документ хорошо освещён" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "Освещение должно быть достаточно хорошим" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "Фото в Вашем документе должно совпадать с фото Вашего лица" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "Фото не соответствуют требованиям?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "Да! Все мои данные совпадают." - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "" - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "" - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "" @@ -17450,11 +17147,11 @@ msgid "New Course" msgstr "" #: cms/templates/index.html -msgid "New Library" +msgid "Email staff to create course" msgstr "" #: cms/templates/index.html -msgid "Email staff to create course" +msgid "New Library" msgstr "" #: cms/templates/index.html @@ -18079,8 +17776,6 @@ msgstr "" msgid "The nuts and bolts of your course" msgstr "" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "" @@ -18140,7 +17835,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -18860,6 +18565,10 @@ msgstr "" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "Это ключевая строка." diff --git a/conf/locale/ru/LC_MESSAGES/djangojs.mo b/conf/locale/ru/LC_MESSAGES/djangojs.mo index 02a2a60b83..44763fdc83 100644 Binary files a/conf/locale/ru/LC_MESSAGES/djangojs.mo and b/conf/locale/ru/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/ru/LC_MESSAGES/djangojs.po b/conf/locale/ru/LC_MESSAGES/djangojs.po index 0477de6bd9..9486e83ee7 100644 --- a/conf/locale/ru/LC_MESSAGES/djangojs.po +++ b/conf/locale/ru/LC_MESSAGES/djangojs.po @@ -99,8 +99,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-26 19:41+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Russian (http://www.transifex.com/projects/p/edx-platform/language/ru/)\n" "MIME-Version: 1.0\n" @@ -150,11 +150,6 @@ msgstr "ОК" msgid "Cancel" msgstr "Отмена" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -284,6 +279,10 @@ msgstr[1] "" msgstr[2] "" msgstr[3] "" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "Ответ:" @@ -3740,6 +3739,19 @@ msgstr "Не удалось отправить заказ" msgid "Could not retrieve payment information" msgstr "" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -3930,6 +3942,10 @@ msgstr "" msgid "OpenAssessment Save Error" msgstr "" +#: cms/static/js/base.js cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "" @@ -4197,6 +4213,9 @@ msgid "" msgstr "" #: cms/static/js/models/uploads.js +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore msgid "or" msgstr "" @@ -5310,6 +5329,45 @@ msgstr "" msgid "Forgot password?" msgstr "" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5336,12 +5394,12 @@ msgid "An error occurred when signing you in to %(platformName)s." msgstr "" #: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" +msgid "or sign in with" msgstr "" #: lms/templates/student_account/login.underscore -msgid "or sign in with" +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" msgstr "" #: lms/templates/student_account/login.underscore @@ -5748,6 +5806,22 @@ msgid "" "photo." msgstr "" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "" @@ -5807,10 +5881,6 @@ msgstr "" msgid "Before proceeding, please confirm that your details match" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.mo b/conf/locale/zh_CN/LC_MESSAGES/django.mo index 8f11a82231..2e98919739 100644 Binary files a/conf/locale/zh_CN/LC_MESSAGES/django.mo and b/conf/locale/zh_CN/LC_MESSAGES/django.mo differ diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.po b/conf/locale/zh_CN/LC_MESSAGES/django.po index 0430f44226..596d65f472 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/django.po +++ b/conf/locale/zh_CN/LC_MESSAGES/django.po @@ -243,7 +243,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:36+0000\n" +"POT-Creation-Date: 2015-06-29 12:25+0000\n" "PO-Revision-Date: 2015-06-18 03:04+0000\n" "Last-Translator: louyihua \n" "Language-Team: Chinese (China) (http://www.transifex.com/projects/p/edx-platform/language/zh_CN/)\n" @@ -851,13 +851,28 @@ msgstr "已经存在与该邮箱地址对应的账户。" msgid "Unable to send email activation link. Please try again later." msgstr "" -#: common/djangoapps/student/views.py -msgid "Name required" -msgstr "需要填写姓名" +#: common/djangoapps/third_party_auth/models.py +msgid "Authentication with {} is currently unavailable." +msgstr "" -#: common/djangoapps/student/views.py -msgid "Invalid ID" -msgstr "无效的ID" +#: common/djangoapps/third_party_auth/models.py +msgid "" +"Secondary providers are displayed less prominently, in a separate list of " +"\"Institution\" login providers." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is enabled, users will not be asked to confirm their details " +"(name, email, etc.) during the registration process. Only select this option" +" for trusted providers that are known to provide accurate user information." +msgstr "" + +#: common/djangoapps/third_party_auth/models.py +msgid "" +"If this option is selected, users will not be required to confirm their " +"email, and their account will be activated immediately upon registration." +msgstr "" #: common/djangoapps/third_party_auth/pipeline.py msgid "" @@ -1302,6 +1317,24 @@ msgstr "未答复" msgid "processing" msgstr "处理中" +#. Translators: these are tooltips that indicate the state of an assessment +#. question +#: common/lib/capa/capa/inputtypes.py +msgid "This is correct." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is incorrect." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is unanswered." +msgstr "" + +#: common/lib/capa/capa/inputtypes.py +msgid "This is being processed." +msgstr "" + #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py msgid "ChoiceGroup: unexpected tag {tag_name}" @@ -2087,6 +2120,16 @@ msgid "" "component with an ORA2 component." msgstr "" +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#. Translators: TBD stands for 'To Be Determined' and is used when a course +#. does not yet have an announced start date. +#: common/lib/xmodule/xmodule/course_metadata_utils.py +#: common/lib/xmodule/xmodule/tests/test_course_metadata_utils.py +#: lms/djangoapps/instructor/paidcourse_enrollment_report.py +msgid "TBD" +msgstr "待定" + #: common/lib/xmodule/xmodule/course_module.py msgid "LTI Passports" msgstr "" @@ -2943,13 +2986,6 @@ msgstr "" msgid "General" msgstr "一般" -#. Translators: TBD stands for 'To Be Determined' and is used when a course -#. does not yet have an announced start date. -#: common/lib/xmodule/xmodule/course_module.py -#: lms/djangoapps/instructor/paidcourse_enrollment_report.py -msgid "TBD" -msgstr "待定" - #: common/lib/xmodule/xmodule/discussion_module.py msgid "Discussion Id" msgstr "" @@ -5883,7 +5919,6 @@ msgstr "ID" #: lms/djangoapps/instructor_task/tasks_helper.py cms/templates/register.html #: lms/templates/register-shib.html lms/templates/register.html #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html -#: lms/templates/verify_student/_modal_editname.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" msgstr "全名" @@ -6713,7 +6748,6 @@ msgid "You must be logged-in to add to a shopping cart" msgstr "您必须登录才能添加到购物车" #: lms/djangoapps/shoppingcart/views.py -#: lms/djangoapps/shoppingcart/tests/test_views.py msgid "The course you requested does not exist." msgstr "您请求的课程不存在。" @@ -7366,17 +7400,6 @@ msgid "" " " msgstr "" -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "Download CSV Reports" -msgstr "下载CSV报告" - -#: lms/djangoapps/shoppingcart/tests/test_views.py -#: lms/templates/shoppingcart/download_report.html -msgid "" -"There was an error in your date input. It should be formatted as YYYY-MM-DD" -msgstr "你输入的日期有误。其格式应为YYYY-MM-DD" - #: lms/djangoapps/student_account/views.py msgid "No user with the provided email address exists." msgstr "" @@ -7479,9 +7502,9 @@ msgstr "" msgid "Payment confirmation" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/djangoapps/verify_student/views.py msgid "Take photo" -msgstr "拍摄" +msgstr "" #: lms/djangoapps/verify_student/views.py msgid "Take a photo of your ID" @@ -8644,6 +8667,14 @@ msgstr "" msgid "Invalid prerequisite course key" msgstr "先修课程标识无效" +#: cms/djangoapps/contentstore/views/course.py +msgid "An error occurred while trying to save your tabs" +msgstr "" + +#: cms/djangoapps/contentstore/views/course.py +msgid "Tabs Exception" +msgstr "" + #: cms/djangoapps/contentstore/views/course.py msgid "This group configuration is in use and cannot be deleted." msgstr "不能删除正在使用中的组配置。" @@ -8905,7 +8936,6 @@ msgid "Loading" msgstr "正在加载" #: cms/templates/asset_index.html lms/templates/courseware/courseware.html -#: lms/templates/verify_student/_modal_editname.html msgid "close" msgstr "关闭" @@ -9045,11 +9075,6 @@ msgstr "帮助" msgid "Sign Out" msgstr "" -#: cms/templates/widgets/tabs-aggregator.html -#: lms/templates/courseware/progress.html -msgid "name" -msgstr "" - #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -9078,10 +9103,6 @@ msgstr "" msgid "Some Rights Reserved" msgstr "" -#: common/templates/course_modes/choose.html -msgid "Upgrade Your Enrollment for {} | Choose Your Track" -msgstr "" - #: common/templates/course_modes/choose.html msgid "Enroll In {} | Choose Your Track" msgstr "" @@ -9090,6 +9111,10 @@ msgstr "" msgid "Sorry, there was an error when trying to enroll you" msgstr "" +#: common/templates/course_modes/choose.html +msgid "Congratulations! You are now enrolled in {course_name}" +msgstr "" + #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" msgstr "" @@ -11806,16 +11831,20 @@ msgstr "" msgid "You have met the requirements for credit in this course." msgstr "" +#: lms/templates/courseware/progress.html +msgid "{link} to purchase course credit." +msgstr "" + #: lms/templates/courseware/progress.html msgid "Go to your dashboard" msgstr "" #: lms/templates/courseware/progress.html -msgid "to purchase course credit." +msgid "You have not yet met the requirements for credit." msgstr "" #: lms/templates/courseware/progress.html -msgid "You have not yet met the requirements for credit." +msgid "display_name" msgstr "" #: lms/templates/courseware/progress.html @@ -11831,8 +11860,7 @@ msgid "Upcoming" msgstr "" #: lms/templates/courseware/progress.html -#: lms/templates/discussion/_underscore_templates.html -msgid "More" +msgid "Less" msgstr "" #: lms/templates/courseware/progress.html @@ -12106,6 +12134,37 @@ msgid "" "{unenroll_link_start}unenroll{unenroll_link_end} from this course" msgstr "" +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"{username}, your eligibility for credit expires on {expiry}. Don't miss out!" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "{congrats} {username}, You have meet requirements for credit." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "Purchase Credit" +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your payment is complete, your credit is processing. Please see " +"{provider_link} for more information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Thank you, your credit is approved. Please see {provider_link} for more " +"information." +msgstr "" + +#: lms/templates/dashboard/_dashboard_credit_information.html +msgid "" +"Your credit has been denied. Please contact {provider_link} for more " +"information." +msgstr "" + #: lms/templates/dashboard/_dashboard_info_language.html msgid "edit" msgstr "编辑" @@ -12579,6 +12638,10 @@ msgstr "" msgid "Open" msgstr "" +#: lms/templates/discussion/_underscore_templates.html +msgid "More" +msgstr "" + #: lms/templates/discussion/_underscore_templates.html #: lms/templates/discussion/mustache/_profile_thread.mustache msgid "anonymous" @@ -15118,10 +15181,19 @@ msgstr "" msgid "Payment" msgstr "" +#: lms/templates/shoppingcart/download_report.html +msgid "Download CSV Reports" +msgstr "下载CSV报告" + #: lms/templates/shoppingcart/download_report.html msgid "Download CSV Data" msgstr "下载CSV数据" +#: lms/templates/shoppingcart/download_report.html +msgid "" +"There was an error in your date input. It should be formatted as YYYY-MM-DD" +msgstr "你输入的日期有误。其格式应为YYYY-MM-DD" + #: lms/templates/shoppingcart/download_report.html msgid "These reports are delimited by start and end dates." msgstr "这些报告按开始和结束日期分隔。" @@ -15436,14 +15508,10 @@ msgid "{platform_name} - Shopping Cart" msgstr "" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Review" msgstr "回顾" #: lms/templates/shoppingcart/shopping_cart_flow.html -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html msgid "Confirmation" msgstr "确认" @@ -15614,101 +15682,14 @@ msgid "" " href=\"{mail_to_link}\"\">{mail_to_link}." msgstr "" -#: lms/templates/verify_student/_modal_editname.html -msgid "Edit Your Name" -msgstr "编辑你的名字" - -#: lms/templates/verify_student/_modal_editname.html -#: lms/templates/verify_student/face_upload.html -msgid "The following error occurred while editing your name:" -msgstr "在编辑你的名字时出现下面的错误:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "" -"To uphold the credibility of {platform} certificates, all name changes will " -"be logged and recorded." -msgstr "为了确保 {platform}证书的可信度,所有姓名变更都将被记录和保存下来。" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Reason for name change:" -msgstr "更改名字原因:" - -#: lms/templates/verify_student/_modal_editname.html -msgid "Change my name" -msgstr "修改我的名字" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Why Do I Need to Re-Verify My Identity?" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"You may need to re-verify your identity if an error occurs with your " -"verification or if your verification has expired. All verifications expire " -"after one year. The re-verification process is the same as the original " -"verification process. You need a webcam and a government-issued photo ID." -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -msgid "Having Technical Trouble?" -msgstr "有技术问题?" - -#: lms/templates/verify_student/_reverification_support.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent" -" version possible{a_end}. Also, please make sure your " -"webcam is plugged in, turned on, and allowed to function in your web" -" browser (commonly adjustable in your browser settings)" -msgstr "" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Have questions?" -msgstr "有疑问?" - -#: lms/templates/verify_student/_reverification_support.html -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please read {a_start}our FAQs to view common questions about our " -"certificates{a_end}." -msgstr "请阅读{a_start}我们的 FAQ 中关于证书的常见问题{a_end}。" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are upgrading your enrollment for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are re-verifying for: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "You are enrolling in: {course_name}" -msgstr "" - -#: lms/templates/verify_student/_verification_header.html -msgid "Congratulations! You are now enrolled in {course_display}" -msgstr "" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "Technical Requirements" -msgstr "技术要求" - -#: lms/templates/verify_student/_verification_support.html -#: lms/templates/verify_student/pay_and_verify.html -msgid "" -"Please make sure your browser is updated to the {a_start}most recent version" -" possible{a_end}. Also, please make sure your webcam is plugged in, " -"turned on, and allowed to function in your web browser (commonly adjustable " -"in your browser settings)." -msgstr "" - #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" msgstr "编辑你的全名" +#: lms/templates/verify_student/face_upload.html +msgid "The following error occurred while editing your name:" +msgstr "在编辑你的名字时出现下面的错误:" + #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" msgstr "" @@ -15739,337 +15720,51 @@ msgstr "为{course_name}认证" msgid "Enroll In {course_name}" msgstr "" -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/pay_and_verify.html +msgid "Have questions?" +msgstr "有疑问?" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please read {a_start}our FAQs to view common questions about our " +"certificates{a_end}." +msgstr "请阅读{a_start}我们的 FAQ 中关于证书的常见问题{a_end}。" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "Technical Requirements" +msgstr "技术要求" + +#: lms/templates/verify_student/pay_and_verify.html +msgid "" +"Please make sure your browser is updated to the {a_start}most recent version" +" possible{a_end}. Also, please make sure your webcam is plugged in, " +"turned on, and allowed to function in your web browser (commonly adjustable " +"in your browser settings)." +msgstr "" + +#: lms/templates/verify_student/reverify.html msgid "Re-Verification" msgstr "重新认证" -#: lms/templates/verify_student/photo_reverification.html -msgid "No Webcam Detected" -msgstr "没有检测到摄像头" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have a webcam connected. Double-check that your webcam is " -"connected and working to continue." -msgstr "没有检测到有效的摄像头。重复确认你的摄像头已经连接而且正在工作然后继续。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "No Flash Detected" -msgstr "没有Flash支持" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"You don't seem to have Flash installed. {a_start} Get Flash {a_end} to " -"continue your registration." -msgstr "你没有安装Flash,请{a_start}获取Flash {a_end}以继续你的注册。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Error submitting your images" -msgstr "提交你的图像时发生错误" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Oops! Something went wrong. Please confirm your details and try again." -msgstr "哎呀!有错误了!请确认你的信息然后再尝试。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Identity" -msgstr "认证你的身份" - -#. Translators: {start_bold} and {end_bold} will be replaced with HTML tags. -#. Please do not translate these variables. -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"To verify your identity and continue as a verified student in this course, " -"complete the following steps {start_bold}before the course verification " -"deadline{end_bold}. If you do not verify your identity, you can still " -"receive an honor code certificate for the course." +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "Identity Verification" msgstr "" -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Progress" -msgstr "你的进度" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Current Step: " -msgstr "当前步骤" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take Photo" -msgstr "重新拍照" - -#: lms/templates/verify_student/photo_reverification.html -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Take ID Photo" -msgstr "重新拍摄身份证件的照片" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Re-Take Your Photo" -msgstr "重拍你的照片" - -#: lms/templates/verify_student/photo_reverification.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "" -"Use your webcam to take a picture of your face so we can match it with the " -"picture on your ID." -msgstr "请用摄像头拍摄一张你面部的照片,以便我们将该照片与你身份证件上的照片进行匹配。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Don't see your picture? Make sure to allow your browser to use your camera " -"when it asks for permission." -msgstr "没有看到你的照片?确认在获取授权时允许你的浏览器使用摄像头。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake" -msgstr "再次拍摄" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Looks good" -msgstr "看起来很好" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Tips on taking a successful photo" -msgstr "成功拍摄的小技巧" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your face is well-lit" -msgstr "确认你的面部被照亮" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be sure your entire face is inside the frame" -msgstr "确认你的面部进入有效区域" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Can we match the photo you took with the one on your ID?" -msgstr "我们能够将你拍摄的相片与你身份证件上的照片进行比对吗?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once in position, use the camera button {btn_icon} to capture your picture" +"You have already submitted your verification information. You will see a " +"message on your dashboard when the verification process is complete (usually" +" within 1-2 days)." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "Use the checkmark button {btn_icon} once you are happy with the photo" +#: lms/templates/verify_student/reverify_not_allowed.html +msgid "You cannot verify your identity at this time." msgstr "" -#: lms/templates/verify_student/photo_reverification.html -msgid "Common Questions" -msgstr "提交问题" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need my photo?" -msgstr "为什么需要我的照片?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"As part of the verification process, we need your photo to confirm that you " -"are you." -msgstr "作为认证程序的一部分,我们需要你的照片来确认你就是你。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "What do you do with this picture?" -msgstr "照片会用在什么地方?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "We only use it to verify your identity. It is not displayed anywhere." -msgstr "我们只将其用于验证你的身份信息,不会在任何其他地方展示。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verification" -msgstr "认证" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your photo looks good, you can move on to step 2." -msgstr "如果确认您拍摄的相片合适,请进入第2步骤。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 2: Re-Take ID Photo" -msgstr "转到步骤2:重新拍摄身份证件的照片" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show Us Your ID" -msgstr "请向我们展示你的身份证件" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Use your webcam to take a picture of your ID so we can match it with your " -"photo and the name on your account." -msgstr "请用摄像头拍摄一张你身份证件的照片,以便我们将该照片与你的面部照片及你在账户中填写的姓名进行匹配。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Make sure your ID is well-lit" -msgstr "确认你的身份证件光线充足" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Acceptable IDs include driver's licenses, passports, or other goverment-" -"issued IDs that include your name and photo" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check that there isn't any glare" -msgstr "检查没有任何炫光" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Ensure that you can see your photo and read your name" -msgstr "确保你可以看到你的照片读出你的名字" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Try to keep your fingers at the edge to avoid covering important information" -msgstr "尽量让你的手指在边缘,以防止遮挡重要信息" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once in position, use the camera button {btn_icon} to capture your ID" -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Why do you need a photo of my ID?" -msgstr "为什么你们需要我的身份证件照片?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We need to match your ID with your photo and name to confirm that you are " -"you." -msgstr "我们需要通过验证你身份证件上的照片及姓名来确认你就是你。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"We encrypt it and send it to our secure authorization service for review. We" -" use the highest levels of security and do not save the photo or information" -" anywhere once the match has been completed." -msgstr "我们会采用最高级别的安全技术将其加密并发送到我们的安全授权服务用于审核目的;一旦审核完成,我们不会继续保存这些照片和信息。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Once you verify your ID photo looks good, you can move on to step 3." -msgstr "一旦你确认身份证件的照片看起来没问题,就可以转到第3步。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Go to Step 3: Review Your Info" -msgstr "转到步骤3:复查你的信息" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Verify Your Submission" -msgstr "验证你的提交" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure we can verify your identity with the photos and information below." -msgstr "请确保我们可以通过以下照片及信息来认证你的身份。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Review the Photos You've Re-Taken" -msgstr "复查你重拍的照片" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Please review the photos and verify that they meet the requirements listed " -"below." -msgstr "请复查照片并且符合下面必需要求" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo above needs to meet the following requirements:" -msgstr "这些照片符合以下需求:" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be well lit" -msgstr "足够明亮" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Show your whole face" -msgstr "清晰显示你整个脸部" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The photo on your ID must match the photo of your face" -msgstr "你身份证件上的照片必须与你的面部照片相符" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Be readable (not too far away, no glare)" -msgstr "可读(不大也不小)" - -#: lms/templates/verify_student/photo_reverification.html -msgid "The name on your ID must match the name on your account below" -msgstr "你身份证件上的名字必须与你在如下账户中填写的名字相一致" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Photos don't meet the requirements?" -msgstr "照片不匹配要求?" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Retake Your Photos" -msgstr "再次拍摄照片" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Check Your Name" -msgstr "检查名字" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Make sure your full name on your {platform_name} account ({full_name}) " -"matches your ID. We will also use this as the name on your certificate." -msgstr "" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Edit your name" -msgstr "编辑你的名字" - -#: lms/templates/verify_student/photo_reverification.html -msgid "" -"Once you verify your details match the requirements, you can move onto to " -"confirm your re-verification submisssion." -msgstr "一旦你验证细节符合要求,你可以继续确认你提交的重新认证请求。" - -#: lms/templates/verify_student/photo_reverification.html -msgid "Yes! My details all match." -msgstr "确定!完全符合。" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "You need to re-verify to continue" -msgstr "再次确认并继续" - -#: lms/templates/verify_student/prompt_midcourse_reverify.html -msgid "" -"To continue in the ID Verified track in {course}, you need to re-verify your" -" identity by {date}. Go to URL." -msgstr "要继续学习{course}中要求经过身份认证的部分,你需要在{date}之前重新认证你的身份,请访问 URL。" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Re-Verification Submission Confirmation" -msgstr "重新认证提交确认" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "Your Credentials Have Been Updated" -msgstr "你的凭据已被更新" - -#: lms/templates/verify_student/reverification_confirmation.html -msgid "" -"We've captured your re-submitted information and will review it to verify " -"your identity shortly. You should receive an update to your veriication " -"status within 1-2 days. In the meantime, you still have access to all of " -"your course content." -msgstr "我们收到了你重新提交的信息,并将稍后进行审查以核实你的身份。你将会在1-2日内收到验证状态的更新。期间你仍可以访问课程的全部内容。" - -#: lms/templates/verify_student/reverification_confirmation.html -#: lms/templates/verify_student/reverification_window_expired.html +#: lms/templates/verify_student/reverify_not_allowed.html msgid "Return to Your Dashboard" msgstr "返回你的课程面板" -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Re-Verification Failed" -msgstr "重新认证失败" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "" -"Your re-verification was submitted after the re-verification deadline, and " -"you can no longer be re-verified." -msgstr "你在重新认证截止期限之后才提交的重新认证请求,你已经不能再重新认证了。" - -#: lms/templates/verify_student/reverification_window_expired.html -msgid "Please contact support if you believe this message to be in error." -msgstr "如果您相信当前消息存在错误,请联系技术支持。" - #: lms/templates/wiki/includes/article_menu.html msgid "{span_start}(active){span_end}" msgstr "{span_start}(激活){span_end}" @@ -17611,14 +17306,14 @@ msgstr "{studio_name}主页" msgid "New Course" msgstr "新建课程" -#: cms/templates/index.html -msgid "New Library" -msgstr "新建知识库" - #: cms/templates/index.html msgid "Email staff to create course" msgstr "发邮件给教员来创建课程" +#: cms/templates/index.html +msgid "New Library" +msgstr "新建知识库" + #: cms/templates/index.html msgid "Please correct the highlighted fields below." msgstr "请改正下面高亮的字段。" @@ -18256,8 +17951,6 @@ msgstr "基本信息" msgid "The nuts and bolts of your course" msgstr "您课程的具体细节" -#. Translators: 'Access to Assessment 1' means the access for a requirement -#. with name 'Assessment 1' #: cms/templates/settings.html msgid "This field is disabled: this information cannot be changed." msgstr "该字段已禁用:信息不可修改。" @@ -18318,7 +18011,17 @@ msgid "Successful Proctored Exam" msgstr "" #: cms/templates/settings.html -msgid "Successful In Course Reverification" +msgid "Proctored Exam {number}" +msgstr "" + +#: cms/templates/settings.html +msgid "Successful In-Course Reverification" +msgstr "" + +#. Translators: 'Access to Assessment 1' means the access for a requirement +#. with name 'Assessment 1' +#: cms/templates/settings.html +msgid "In-Course Reverification {number}" msgstr "" #: cms/templates/settings.html @@ -19057,6 +18760,10 @@ msgstr "如何使用{studio_name}建立您的课程" msgid "Have problems, questions, or suggestions about {studio_name}?" msgstr "对{studio_name}有问题、疑问或者建议?" +#: cms/templates/widgets/tabs-aggregator.html +msgid "name" +msgstr "" + # empty msgid "This is a key string." msgstr "这是一个关键字字符串。" diff --git a/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo b/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo index 92a05db36a..bc085dc84c 100644 Binary files a/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo and b/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/zh_CN/LC_MESSAGES/djangojs.po b/conf/locale/zh_CN/LC_MESSAGES/djangojs.po index a023379e1b..06b555e261 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/djangojs.po +++ b/conf/locale/zh_CN/LC_MESSAGES/djangojs.po @@ -122,8 +122,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-06-19 13:34+0000\n" -"PO-Revision-Date: 2015-06-19 13:38+0000\n" +"POT-Creation-Date: 2015-06-29 12:24+0000\n" +"PO-Revision-Date: 2015-06-26 19:41+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Chinese (China) (http://www.transifex.com/projects/p/edx-platform/language/zh_CN/)\n" "MIME-Version: 1.0\n" @@ -185,11 +185,6 @@ msgstr "是的" msgid "Cancel" msgstr "取消" -#: cms/static/js/base.js lms/static/js/verify_student/photocapture.js -#: cms/templates/js/checklist.underscore -msgid "This link will open in a new browser window/tab" -msgstr "该链接将会在新的浏览器窗口/标签打开" - #: cms/static/js/certificates/views/signatory_editor.js #: cms/static/js/views/asset.js cms/static/js/views/list_item.js #: cms/static/js/views/manage_users_and_roles.js @@ -327,6 +322,10 @@ msgid "(%(num_points)s point possible)" msgid_plural "(%(num_points)s points possible)" msgstr[0] "(本题共有%(num_points)s分)" +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "The grading process is still running. Refresh the page to see updates." +msgstr "" + #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "Answer:" msgstr "回答:" @@ -3724,6 +3723,18 @@ msgstr "订单提交失败" msgid "Could not retrieve payment information" msgstr "无法读取支付信息" +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Take a photo of your ID" +msgstr "" + +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Review your info" +msgstr "" + +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "确认" + #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "发生了一个错误。请重新加载这个页面。" @@ -3898,6 +3909,10 @@ msgstr "删除中" msgid "OpenAssessment Save Error" msgstr "开放式评估保存错误" +#: cms/templates/js/checklist.underscore +msgid "This link will open in a new browser window/tab" +msgstr "该链接将会在新的浏览器窗口/标签打开" + #: cms/static/js/base.js msgid "This link will open in a modal window" msgstr "该链接将在一个模式窗口中打开" @@ -5272,6 +5287,45 @@ msgstr "" msgid "Forgot password?" msgstr "忘记密码?" +#: lms/templates/student_account/hinted_login.underscore +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Sign in" +msgstr "登录" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Would you like to sign in using your %(providerName)s credentials?" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Sign in using %(providerName)s" +msgstr "" + +#: lms/templates/student_account/hinted_login.underscore +msgid "Show me other ways to sign in or register" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Sign in with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +#: lms/templates/student_account/institution_register.underscore +msgid "Choose your institution from the list below:" +msgstr "" + +#: lms/templates/student_account/institution_login.underscore +msgid "Back to sign in" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register with Institution/Campus Credentials" +msgstr "" + +#: lms/templates/student_account/institution_register.underscore +msgid "Register through edX" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "" "You have successfully signed into %(currentProvider)s, but your " @@ -5297,15 +5351,15 @@ msgstr "我们无法让您登录。" msgid "An error occurred when signing you in to %(platformName)s." msgstr "" -#: lms/templates/student_account/login.underscore -#: lms/templates/student_account/register.underscore -msgid "Sign in" -msgstr "登录" - #: lms/templates/student_account/login.underscore msgid "or sign in with" msgstr "或者通过以下方式登录" +#: lms/templates/student_account/login.underscore +#: lms/templates/student_account/register.underscore +msgid "Use my institution/campus credentials" +msgstr "" + #: lms/templates/student_account/login.underscore msgid "New to %(platformName)s?" msgstr "刚刚接触%(platformName)s?" @@ -5713,6 +5767,22 @@ msgid "" "photo." msgstr "驾照、护照或者由政府签发的带有你姓名和照片的身份证件。" +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Identity Verification In Progress" +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "" +"We have received your information and are verifying your identity. You will " +"see a message on your dashboard when the verification process is complete " +"(usually within 1-2 days). In the meantime, you can still access all " +"available course content." +msgstr "" + +#: lms/templates/verify_student/reverify_success_step.underscore +msgid "Return to Your Dashboard" +msgstr "" + #: lms/templates/verify_student/review_photos_step.underscore msgid "Review Your Photos" msgstr "复核你的照片" @@ -5772,10 +5842,6 @@ msgstr "重拍你的照片" msgid "Before proceeding, please confirm that your details match" msgstr "在进行下一步之前,请确认你提供的信息之间相符" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "确认" - #: lms/templates/verify_student/webcam_photo.underscore msgid "" "Don't see your picture? Make sure to allow your browser to use your camera " diff --git a/docs/config.ini b/docs/config.ini index 01ce1e72ef..e1f5999618 100644 --- a/docs/config.ini +++ b/docs/config.ini @@ -39,7 +39,7 @@ content_libraries = creating_content/libraries.html content_groups = cohorts/cohorted_courseware.html group_configurations = content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio container = developing_course/course_components.html#components-that-contain-other-components -video = index.html +video = video/video_uploads.html certificates = building_course/creating_course_certificates.html # below are the language directory names for the different locales diff --git a/lms/djangoapps/ccx/plugins.py b/lms/djangoapps/ccx/plugins.py index 61d01009aa..5408dbdaf7 100644 --- a/lms/djangoapps/ccx/plugins.py +++ b/lms/djangoapps/ccx/plugins.py @@ -3,7 +3,7 @@ Registers the CCX feature for the edX platform. """ from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_noop from xmodule.tabs import CourseTab from student.roles import CourseCcxCoachRole @@ -15,7 +15,7 @@ class CcxCourseTab(CourseTab): """ type = "ccx_coach" - title = _("CCX Coach") + title = ugettext_noop("CCX Coach") view_name = "ccx_coach_dashboard" is_dynamic = True # The CCX view is dynamically added to the set of tabs when it is enabled diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 5bacdd2d98..3b98b3b46b 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -143,7 +143,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, with self.assertNumQueries(queries): with check_mongo_calls(reads): - with check_sum_of_calls(XBlock, ['__init__'], xblocks): + with check_sum_of_calls(XBlock, ['__init__'], xblocks, xblocks, include_arguments=False): self.grade_course(self.course) @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(1, 4), (True, False))) @@ -173,18 +173,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { # (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks - ('no_overrides', 1, True): (27, 7, 19), - ('no_overrides', 2, True): (135, 7, 131), - ('no_overrides', 3, True): (595, 7, 537), - ('ccx', 1, True): (27, 7, 47), - ('ccx', 2, True): (135, 7, 455), - ('ccx', 3, True): (595, 7, 2037), - ('no_overrides', 1, False): (27, 7, 19), - ('no_overrides', 2, False): (135, 7, 131), - ('no_overrides', 3, False): (595, 7, 537), - ('ccx', 1, False): (27, 7, 19), - ('ccx', 2, False): (135, 7, 131), - ('ccx', 3, False): (595, 7, 537), + ('no_overrides', 1, True): (26, 7, 14), + ('no_overrides', 2, True): (134, 7, 85), + ('no_overrides', 3, True): (594, 7, 336), + ('ccx', 1, True): (26, 7, 14), + ('ccx', 2, True): (134, 7, 85), + ('ccx', 3, True): (594, 7, 336), + ('no_overrides', 1, False): (26, 7, 14), + ('no_overrides', 2, False): (134, 7, 85), + ('no_overrides', 3, False): (594, 7, 336), + ('ccx', 1, False): (26, 7, 14), + ('ccx', 2, False): (134, 7, 85), + ('ccx', 3, False): (594, 7, 336), } @@ -196,16 +196,16 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - ('no_overrides', 1, True): (27, 4, 9), - ('no_overrides', 2, True): (135, 19, 54), - ('no_overrides', 3, True): (595, 84, 215), - ('ccx', 1, True): (27, 4, 9), - ('ccx', 2, True): (135, 19, 54), - ('ccx', 3, True): (595, 84, 215), - ('no_overrides', 1, False): (27, 4, 9), - ('no_overrides', 2, False): (135, 19, 54), - ('no_overrides', 3, False): (595, 84, 215), - ('ccx', 1, False): (27, 4, 9), - ('ccx', 2, False): (135, 19, 54), - ('ccx', 3, False): (595, 84, 215), + ('no_overrides', 1, True): (26, 4, 9), + ('no_overrides', 2, True): (134, 19, 54), + ('no_overrides', 3, True): (594, 84, 215), + ('ccx', 1, True): (26, 4, 9), + ('ccx', 2, True): (134, 19, 54), + ('ccx', 3, True): (594, 84, 215), + ('no_overrides', 1, False): (26, 4, 9), + ('no_overrides', 2, False): (134, 19, 54), + ('no_overrides', 3, False): (594, 84, 215), + ('ccx', 1, False): (26, 4, 9), + ('ccx', 2, False): (134, 19, 54), + ('ccx', 3, False): (594, 84, 215), } diff --git a/lms/djangoapps/commerce/tests/test_views.py b/lms/djangoapps/commerce/tests/test_views.py index 0c88269c8b..4a5cbc5a47 100644 --- a/lms/djangoapps/commerce/tests/test_views.py +++ b/lms/djangoapps/commerce/tests/test_views.py @@ -5,6 +5,7 @@ from uuid import uuid4 from nose.plugins.attrib import attr import ddt +from django.conf import settings from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings @@ -17,6 +18,8 @@ from commerce.constants import Messages from commerce.tests import TEST_BASKET_ID, TEST_ORDER_NUMBER, TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY from commerce.tests.mocks import mock_basket_order, mock_create_basket from course_modes.models import CourseMode +from embargo.test_utils import restrict_course +from openedx.core.lib.django_test_client_utils import get_absolute_url from enrollment.api import get_enrollment from student.models import CourseEnrollment from student.tests.factories import UserFactory, CourseModeFactory @@ -42,7 +45,6 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) """ Tests for the commerce orders view. """ - def _post_to_view(self, course_id=None): """ POST to the view being tested. @@ -96,6 +98,17 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) # Ignore events fired from UserFactory creation self.reset_tracker() + @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) + def test_embargo_restriction(self): + """ + The view should return HTTP 403 status if the course is embargoed. + """ + with restrict_course(self.course.id) as redirect_url: + response = self._post_to_view() + self.assertEqual(403, response.status_code) + body = json.loads(response.content) + self.assertEqual(get_absolute_url(redirect_url), body['user_message_url']) + def test_login_required(self): """ The view should return HTTP 403 status if the user is not logged in. diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py index d3e2a80a34..03394c4ebb 100644 --- a/lms/djangoapps/commerce/views.py +++ b/lms/djangoapps/commerce/views.py @@ -20,6 +20,7 @@ from course_modes.models import CourseMode from courseware import courses from edxmako.shortcuts import render_to_response from enrollment.api import add_enrollment +from embargo import api as embargo_api from microsite_configuration import microsite from student.models import CourseEnrollment from openedx.core.lib.api.authentication import SessionAuthenticationAllowInactiveUser @@ -76,6 +77,11 @@ class BasketsView(APIView): if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) + embargo_response = embargo_api.get_embargo_response(request, course_key, user) + + if embargo_response: + return embargo_response + # Don't do anything if an enrollment already exists course_id = unicode(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) diff --git a/lms/djangoapps/course_structure_api/v0/views.py b/lms/djangoapps/course_structure_api/v0/views.py index 2d31995751..08f8f56df9 100644 --- a/lms/djangoapps/course_structure_api/v0/views.py +++ b/lms/djangoapps/course_structure_api/v0/views.py @@ -560,7 +560,12 @@ class CourseBlocksAndNavigation(ListAPIView): ) # verify the user has access to this block - if not has_access(request_info.request.user, 'load', block_info.block, course_key=request_info.course.id): + if (block_info.block is None or not has_access( + request_info.request.user, + 'load', + block_info.block, + course_key=request_info.course.id + )): return # add the block's value to the result diff --git a/lms/djangoapps/course_wiki/tab.py b/lms/djangoapps/course_wiki/tab.py index eecc082dcb..e825ea8087 100644 --- a/lms/djangoapps/course_wiki/tab.py +++ b/lms/djangoapps/course_wiki/tab.py @@ -4,7 +4,7 @@ a user has on an article. """ from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_noop from courseware.tabs import EnrolledTab @@ -15,9 +15,10 @@ class WikiTab(EnrolledTab): """ type = "wiki" - title = _('Wiki') + title = ugettext_noop('Wiki') view_name = "course_wiki" is_hideable = True + is_default = False @classmethod def is_enabled(cls, course, user=None): diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 3fa2cda60c..3d41f5525d 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -663,17 +663,19 @@ def _has_staff_access_to_descriptor(user, descriptor, course_key): return _has_staff_access_to_location(user, descriptor.location, course_key) -def is_mobile_available_for_user(user, course): +def is_mobile_available_for_user(user, descriptor): """ Returns whether the given course is mobile_available for the given user. Checks: mobile_available flag on the course Beta User and staff access overrides the mobile_available flag + Arguments: + descriptor (CourseDescriptor|CourseOverview): course or overview of course in question """ return ( - course.mobile_available or - auth.has_access(user, CourseBetaTesterRole(course.id)) or - _has_staff_access_to_descriptor(user, course, course.id) + descriptor.mobile_available or + auth.has_access(user, CourseBetaTesterRole(descriptor.id)) or + _has_staff_access_to_descriptor(user, descriptor, descriptor.id) ) diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index b1e0359f62..58c0e90040 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -176,11 +176,11 @@ Feature: LMS.Answer problems Scenario: I can view and hide the answer if the problem has it: Given I am viewing a "numerical" that shows the answer "always" - When I press the button with the label "Show Answer" - Then the Show/Hide button label is "Hide Answer" + When I press the button with the label "SHOW ANSWER" + Then the Show/Hide button label is "HIDE ANSWER" And I should see "4.14159" somewhere in the page - When I press the button with the label "Hide Answer" - Then the Show/Hide button label is "Show Answer" + When I press the button with the label "HIDE ANSWER" + Then the Show/Hide button label is "SHOW ANSWER" And I should not see "4.14159" anywhere on the page Scenario: I can see my score on a problem when I answer it and after I reset it diff --git a/lms/djangoapps/courseware/features/problems_setup.py b/lms/djangoapps/courseware/features/problems_setup.py index e7ac96a535..b4a0aa135d 100644 --- a/lms/djangoapps/courseware/features/problems_setup.py +++ b/lms/djangoapps/courseware/features/problems_setup.py @@ -84,7 +84,7 @@ PROBLEM_DICT = { 'answer': 'correct string'}, 'correct': ['div.correct'], 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered']}, + 'unanswered': ['div.unanswered', 'div.unsubmitted']}, 'numerical': { 'factory': NumericalResponseXMLFactory(), @@ -95,7 +95,7 @@ PROBLEM_DICT = { 'math_display': True}, 'correct': ['div.correct'], 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered']}, + 'unanswered': ['div.unanswered', 'div.unsubmitted']}, 'formula': { 'factory': FormulaResponseXMLFactory(), @@ -108,7 +108,7 @@ PROBLEM_DICT = { 'answer': 'x^2+2*x+y'}, 'correct': ['div.correct'], 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered']}, + 'unanswered': ['div.unanswered', 'div.unsubmitted']}, 'script': { 'factory': CustomResponseXMLFactory(), @@ -129,7 +129,7 @@ PROBLEM_DICT = { """)}, 'correct': ['div.correct'], 'incorrect': ['div.incorrect'], - 'unanswered': ['div.unanswered']}, + 'unanswered': ['div.unanswered', 'div.unsubmitted']}, 'code': { 'factory': CodeResponseXMLFactory(), diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 3c2cd72026..ac7b72520b 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -26,6 +26,7 @@ from student.models import user_by_anonymous_id from submissions.models import score_set, score_reset from xmodule_django.models import CourseKeyField, LocationKeyField, BlockTypeKeyField # pylint: disable=import-error +log = logging.getLogger(__name__) log = logging.getLogger("edx.courseware") @@ -72,7 +73,6 @@ class StudentModule(models.Model): Keeps student state for a particular module in a particular course. """ objects = ChunkingManager() - MODEL_TAGS = ['course_id', 'module_type'] # For a homework problem, contains a JSON @@ -96,10 +96,10 @@ class StudentModule(models.Model): class Meta(object): # pylint: disable=missing-docstring unique_together = (('student', 'module_state_key', 'course_id'),) - ## Internal state of the object + # Internal state of the object state = models.TextField(null=True, blank=True) - ## Grade, and are we done? + # Grade, and are we done? grade = models.FloatField(null=True, blank=True, db_index=True) max_grade = models.FloatField(null=True, blank=True) DONE_TYPES = ( @@ -146,7 +146,6 @@ class StudentModuleHistory(models.Model): """Keeps a complete history of state changes for a given XModule for a given Student. Right now, we restrict this to problems so that the table doesn't explode in size.""" - HISTORY_SAVING_TYPES = {'problem'} class Meta(object): # pylint: disable=missing-docstring @@ -211,7 +210,6 @@ class XModuleUserStateSummaryField(XBlockFieldBase): """ Stores data set in the Scope.user_state_summary scope by an xmodule field """ - class Meta(object): # pylint: disable=missing-docstring unique_together = (('usage_id', 'field_name'),) @@ -223,7 +221,6 @@ class XModuleStudentPrefsField(XBlockFieldBase): """ Stores data set in the Scope.preferences scope by an xmodule field """ - class Meta(object): # pylint: disable=missing-docstring unique_together = (('student', 'module_type', 'field_name'),) @@ -237,10 +234,8 @@ class XModuleStudentInfoField(XBlockFieldBase): """ Stores data set in the Scope.preferences scope by an xmodule field """ - class Meta(object): # pylint: disable=missing-docstring unique_together = (('student', 'field_name'),) - student = models.ForeignKey(User, db_index=True) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 94b4b25d88..d23de14ceb 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -3,7 +3,7 @@ This module is essentially a broker to xmodule/tabs.py -- it was originally intr perform some LMS-specific tab display gymnastics for the Entrance Exams feature """ from django.conf import settings -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ugettext_noop from courseware.access import has_access from courseware.entrance_exams import user_must_complete_entrance_exam @@ -28,10 +28,11 @@ class CoursewareTab(EnrolledTab): The main courseware view. """ type = 'courseware' - title = _('Courseware') + title = ugettext_noop('Courseware') priority = 10 view_name = 'courseware' is_movable = False + is_default = False class CourseInfoTab(CourseTab): @@ -39,11 +40,12 @@ class CourseInfoTab(CourseTab): The course info view. """ type = 'course_info' - title = _('Course Info') + title = ugettext_noop('Course Info') priority = 20 view_name = 'info' tab_id = 'info' is_movable = False + is_default = False @classmethod def is_enabled(cls, course, user=None): @@ -55,10 +57,11 @@ class SyllabusTab(EnrolledTab): A tab for the course syllabus. """ type = 'syllabus' - title = _('Syllabus') + title = ugettext_noop('Syllabus') priority = 30 view_name = 'syllabus' allow_multiple = True + is_default = False @classmethod def is_enabled(cls, course, user=None): # pylint: disable=unused-argument @@ -72,10 +75,11 @@ class ProgressTab(EnrolledTab): The course progress view. """ type = 'progress' - title = _('Progress') + title = ugettext_noop('Progress') priority = 40 view_name = 'progress' is_hideable = True + is_default = False @classmethod def is_enabled(cls, course, user=None): # pylint: disable=unused-argument @@ -89,8 +93,9 @@ class TextbookTabsBase(CourseTab): Abstract class for textbook collection tabs classes. """ # Translators: 'Textbooks' refers to the tab in the course that leads to the course' textbooks - title = _("Textbooks") + title = ugettext_noop("Textbooks") is_collection = True + is_default = False @classmethod def is_enabled(cls, course, user=None): # pylint: disable=unused-argument @@ -220,8 +225,9 @@ class ExternalDiscussionCourseTab(LinkTab): type = 'external_discussion' # Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums - title = _('Discussion') + title = ugettext_noop('Discussion') priority = None + is_default = False @classmethod def validate(cls, tab_dict, raise_error=True): diff --git a/lms/djangoapps/courseware/tests/test_i18n.py b/lms/djangoapps/courseware/tests/test_i18n.py index a67442e64b..e5ae1b6f19 100644 --- a/lms/djangoapps/courseware/tests/test_i18n.py +++ b/lms/djangoapps/courseware/tests/test_i18n.py @@ -4,37 +4,65 @@ Tests i18n in courseware import re from nose.plugins.attrib import attr +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse, NoReverseMatch from django.test import TestCase -from django.test.utils import override_settings +from django.test.client import Client + +from dark_lang.models import DarkLangConfig +from lang_pref import LANGUAGE_KEY +from openedx.core.djangoapps.user_api.preferences.api import set_user_preference +from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory -@attr('shard_1') -@override_settings(LANGUAGES=[('eo', 'Esperanto'), ('ar', 'Arabic')]) -class I18nTestCase(TestCase): +class BaseI18nTestCase(TestCase): """ - Tests for i18n + Base utilities for i18n test classes to derive from """ def assert_tag_has_attr(self, content, tag, attname, value): """Assert that a tag in `content` has a certain value in a certain attribute.""" - regex = r"""<{tag} [^>]*\b{attname}=['"]([\w\d ]+)['"][^>]*>""".format(tag=tag, attname=attname) + regex = r"""<{tag} [^>]*\b{attname}=['"]([\w\d\- ]+)['"][^>]*>""".format(tag=tag, attname=attname) match = re.search(regex, content) - self.assertTrue(match, "Couldn't find desired tag in %r" % content) + self.assertTrue(match, "Couldn't find desired tag '%s' with attr '%s' in %r" % (tag, attname, content)) attvalues = match.group(1).split() self.assertIn(value, attvalues) + def release_languages(self, languages): + """ + Release a set of languages using the dark lang interface. + languages is a list of comma-separated lang codes, eg, 'ar, es-419' + """ + user = User() + user.save() + DarkLangConfig( + released_languages=languages, + changed_by=user, + enabled=True + ).save() + + +@attr('shard_1') +class I18nTestCase(BaseI18nTestCase): + """ + Tests for i18n + """ def test_default_is_en(self): + self.release_languages('fr') response = self.client.get('/') self.assert_tag_has_attr(response.content, "html", "lang", "en") self.assertEqual(response['Content-Language'], 'en') self.assert_tag_has_attr(response.content, "body", "class", "lang_en") def test_esperanto(self): + self.release_languages('fr, eo') response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='eo') self.assert_tag_has_attr(response.content, "html", "lang", "eo") self.assertEqual(response['Content-Language'], 'eo') self.assert_tag_has_attr(response.content, "body", "class", "lang_eo") def test_switching_languages_bidi(self): + self.release_languages('ar, eo') response = self.client.get('/') self.assert_tag_has_attr(response.content, "html", "lang", "en") self.assertEqual(response['Content-Language'], 'en') @@ -46,3 +74,122 @@ class I18nTestCase(TestCase): self.assertEqual(response['Content-Language'], 'ar') self.assert_tag_has_attr(response.content, "body", "class", "lang_ar") self.assert_tag_has_attr(response.content, "body", "class", "rtl") + + +@attr('shard_1') +class I18nRegressionTests(BaseI18nTestCase): + """ + Tests for i18n + """ + def test_es419_acceptance(self): + # Regression test; LOC-72, and an issue with Django + self.release_languages('es-419') + response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='es-419') + self.assert_tag_has_attr(response.content, "html", "lang", "es-419") + + def test_unreleased_lang_resolution(self): + # Regression test; LOC-85 + self.release_languages('fa') + + # We've released 'fa', AND we have language files for 'fa-ir' but + # we want to keep 'fa-ir' as a dark language. Requesting 'fa-ir' + # in the http request (NOT with the ?preview-lang query param) should + # receive files for 'fa' + response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fa-ir') + self.assert_tag_has_attr(response.content, "html", "lang", "fa") + + # Now try to access with dark lang + response = self.client.get('/?preview-lang=fa-ir') + self.assert_tag_has_attr(response.content, "html", "lang", "fa-ir") + + def test_preview_lang(self): + # Regression test; LOC-87 + self.release_languages('es-419') + site_lang = settings.LANGUAGE_CODE + # Visit the front page; verify we see site default lang + response = self.client.get('/') + self.assert_tag_has_attr(response.content, "html", "lang", site_lang) + + # Verify we can switch language using the preview-lang query param + response = self.client.get('/?preview-lang=eo') + self.assert_tag_has_attr(response.content, "html", "lang", "eo") + # We should be able to see released languages using preview-lang, too + response = self.client.get('/?preview-lang=es-419') + self.assert_tag_has_attr(response.content, "html", "lang", "es-419") + + # Clearing the language should go back to site default + response = self.client.get('/?clear-lang') + self.assert_tag_has_attr(response.content, "html", "lang", site_lang) + + +@attr('shard_1') +class I18nLangPrefTests(BaseI18nTestCase): + """ + Regression tests of language presented to the user, when they + choose a language preference, and when they have a preference + and use the dark lang preview functionality. + """ + def setUp(self): + super(I18nLangPrefTests, self).setUp() + # Create one user and save it to the database + email = 'test@edx.org' + pwd = 'test_password' + self.user = UserFactory.build(username='test', email=email) + self.user.set_password(pwd) + self.user.save() + + # Create a registration for the user + RegistrationFactory(user=self.user) + + # Create a profile for the user + UserProfileFactory(user=self.user) + + # Create the test client + self.client = Client() + + # Get the login url & log in our user + try: + login_url = reverse('login_post') + except NoReverseMatch: + login_url = reverse('login') + self.client.post(login_url, {'email': email, 'password': pwd}) + + # Url and site lang vars for tests to use + self.url = reverse('dashboard') + self.site_lang = settings.LANGUAGE_CODE + + def test_lang_preference(self): + # Regression test; LOC-87 + self.release_languages('ar, es-419') + + # Visit the front page; verify we see site default lang + response = self.client.get(self.url) + self.assert_tag_has_attr(response.content, "html", "lang", self.site_lang) + + # Set user language preference + set_user_preference(self.user, LANGUAGE_KEY, 'ar') + # and verify we now get an ar response + response = self.client.get(self.url) + self.assert_tag_has_attr(response.content, "html", "lang", 'ar') + + # Verify that switching language preference gives the right language + set_user_preference(self.user, LANGUAGE_KEY, 'es-419') + response = self.client.get(self.url) + self.assert_tag_has_attr(response.content, "html", "lang", 'es-419') + + def test_preview_precedence(self): + # Regression test; LOC-87 + self.release_languages('ar, es-419') + + # Set user language preference + set_user_preference(self.user, LANGUAGE_KEY, 'ar') + # Verify preview-lang takes precedence + response = self.client.get('{}?preview-lang=eo'.format(self.url)) + self.assert_tag_has_attr(response.content, "html", "lang", 'eo') + # Hitting another page should keep the dark language set. + response = self.client.get(reverse('courses')) + self.assert_tag_has_attr(response.content, "html", "lang", "eo") + + # Clearing language must set language back to preference language + response = self.client.get('{}?clear-lang'.format(self.url)) + self.assert_tag_has_attr(response.content, "html", "lang", 'ar') diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 408f763178..3459f4eb46 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -610,11 +610,11 @@ class TestTOC(ModuleStoreTestCase): # Split makes 6 queries to load the course to depth 2: # - load the structure # - load 5 definitions - # Split makes 6 queries to render the toc: + # Split makes 5 queries to render the toc: # - it loads the active version at the start of the bulk operation - # - it loads 5 definitions, because it instantiates the a CourseModule and 4 VideoModules + # - it loads 4 definitions, because it instantiates 4 VideoModules # each of which access a Scope.content field in __init__ - @ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 6)) + @ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 5)) @ddt.unpack def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds): with self.store.default_store(default_ms): @@ -634,9 +634,10 @@ class TestTOC(ModuleStoreTestCase): 'format': '', 'due': None, 'active': False}], 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) + course = self.store.get_course(self.toy_course.id, depth=2) with check_mongo_calls(toc_finds): actual = render.toc_for_course( - self.request, self.toy_course, self.chapter, None, self.field_data_cache + self.request, course, self.chapter, None, self.field_data_cache ) for toc_section in expected: self.assertIn(toc_section, actual) @@ -648,11 +649,11 @@ class TestTOC(ModuleStoreTestCase): # Split makes 6 queries to load the course to depth 2: # - load the structure # - load 5 definitions - # Split makes 2 queries to render the toc: + # Split makes 5 queries to render the toc: # - it loads the active version at the start of the bulk operation - # - it loads 5 definitions, because it instantiates the a CourseModule and 4 VideoModules + # - it loads 4 definitions, because it instantiates 4 VideoModules # each of which access a Scope.content field in __init__ - @ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 6)) + @ddt.data((ModuleStoreEnum.Type.mongo, 3, 0, 0), (ModuleStoreEnum.Type.split, 6, 0, 5)) @ddt.unpack def test_toc_toy_from_section(self, default_ms, setup_finds, setup_sends, toc_finds): with self.store.default_store(default_ms): diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py index 9e52a73c72..b50d4d793a 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -480,6 +480,7 @@ class TabListTestCase(TabTestCase): [{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}], # incorrect order [{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}], + [{'type': 'unknown_type'}] ] # tab types that should appear only once diff --git a/lms/djangoapps/courseware/user_state_client.py b/lms/djangoapps/courseware/user_state_client.py index a60284cfbd..bb18d1d7ab 100644 --- a/lms/djangoapps/courseware/user_state_client.py +++ b/lms/djangoapps/courseware/user_state_client.py @@ -13,7 +13,7 @@ except ImportError: from django.contrib.auth.models import User from xblock.fields import Scope, ScopeBase -from xblock_user_state.interface import XBlockUserStateClient +from edx_user_state_client.interface import XBlockUserStateClient from courseware.models import StudentModule, StudentModuleHistory from contracts import contract, new_contract from opaque_keys.edx.keys import UsageKey diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index f9db7beb9e..de5252bbfe 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -7,6 +7,7 @@ import urllib import json import cgi +from collections import OrderedDict from datetime import datetime from django.utils import translation from django.utils.translation import ugettext as _ @@ -1065,21 +1066,6 @@ def _progress(request, course_key, student_id): # checking certificate generation configuration show_generate_cert_btn = certs_api.cert_generation_enabled(course_key) - if is_credit_course(course_key): - requirement_statuses = get_credit_requirement_status(course_key, student.username) - if any(requirement['status'] == 'failed' for requirement in requirement_statuses): - eligibility_status = "not_eligible" - elif is_user_eligible_for_credit(student.username, course_key): - eligibility_status = "eligible" - else: - eligibility_status = "partial_eligible" - credit_course = { - 'eligibility_status': eligibility_status, - 'requirements': requirement_statuses - } - else: - credit_course = None - context = { 'course': course, 'courseware_summary': courseware_summary, @@ -1089,7 +1075,7 @@ def _progress(request, course_key, student_id): 'student': student, 'passed': is_course_passed(course, grade_summary), 'show_generate_cert_btn': show_generate_cert_btn, - 'credit_course': credit_course + 'credit_course_requirements': _credit_course_requirements(course_key, student), } if show_generate_cert_btn: @@ -1120,6 +1106,64 @@ def _progress(request, course_key, student_id): return response +def _credit_course_requirements(course_key, student): + """Return information about which credit requirements a user has satisfied. + + Arguments: + course_key (CourseKey): Identifier for the course. + student (User): Currently logged in user. + + Returns: dict + + """ + # If credit eligibility is not enabled or this is not a credit course, + # short-circuit and return `None`. This indicates that credit requirements + # should NOT be displayed on the progress page. + if not (settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course(course_key)): + return None + + # Retrieve the status of the user for each eligibility requirement in the course. + # For each requirement, the user's status is either "satisfied", "failed", or None. + # In this context, `None` means that we don't know the user's status, either because + # the user hasn't done something (for example, submitting photos for verification) + # or we're waiting on more information (for example, a response from the photo + # verification service). + requirement_statuses = get_credit_requirement_status(course_key, student.username) + + # If the user has been marked as "eligible", then they are *always* eligible + # unless someone manually intervenes. This could lead to some strange behavior + # if the requirements change post-launch. For example, if the user was marked as eligible + # for credit, then a new requirement was added, the user will see that they're eligible + # AND that one of the requirements is still pending. + # We're assuming here that (a) we can mitigate this by properly training course teams, + # and (b) it's a better user experience to allow students who were at one time + # marked as eligible to continue to be eligible. + # If we need to, we can always manually move students back to ineligible by + # deleting CreditEligibility records in the database. + if is_user_eligible_for_credit(student.username, course_key): + eligibility_status = "eligible" + + # If the user has *failed* any requirements (for example, if a photo verification is denied), + # then the user is NOT eligible for credit. + elif any(requirement['status'] == 'failed' for requirement in requirement_statuses): + eligibility_status = "not_eligible" + + # Otherwise, the user may be eligible for credit, but the user has not + # yet completed all the requirements. + else: + eligibility_status = "partial_eligible" + + paired_requirements = {} + for requirement in requirement_statuses: + namespace = requirement.pop("namespace") + paired_requirements.setdefault(namespace, []).append(requirement) + + return { + 'eligibility_status': eligibility_status, + 'requirements': OrderedDict(sorted(paired_requirements.items(), reverse=True)) + } + + @login_required @ensure_valid_course_key def submission_history(request, course_id, student_username, location): diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index c307140552..a3a81b33ce 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -92,14 +92,28 @@ def _get_comment_and_context(request, comment_id): raise Http404 -def get_thread_list_url(request, course_key, topic_id_list=None): +def _is_user_author_or_privileged(cc_content, context): + """ + Check if the user is the author of a content object or a privileged user. + + Returns: + Boolean + """ + return ( + context["is_requester_privileged"] or + context["cc_requester"]["id"] == cc_content["user_id"] + ) + + +def get_thread_list_url(request, course_key, topic_id_list=None, following=False): """ Returns the URL for the thread_list_url field, given a list of topic_ids """ path = reverse("thread-list") query_list = ( [("course_id", unicode(course_key))] + - [("topic_id", topic_id) for topic_id in topic_id_list or []] + [("topic_id", topic_id) for topic_id in topic_id_list or []] + + ([("following", following)] if following else []) ) return request.build_absolute_uri(urlunparse(("", "", path, "", urlencode(query_list), ""))) @@ -132,7 +146,8 @@ def get_course(request, course_key): {"start": blackout["start"].isoformat(), "end": blackout["end"].isoformat()} for blackout in course.get_discussion_blackout_datetimes() ], - "thread_list_url": get_thread_list_url(request, course_key, topic_id_list=[]), + "thread_list_url": get_thread_list_url(request, course_key), + "following_thread_list_url": get_thread_list_url(request, course_key, following=True), "topics_url": request.build_absolute_uri( reverse("course_topics", kwargs={"course_id": course_key}) ) @@ -211,7 +226,7 @@ def get_course_topics(request, course_key): } -def get_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None): +def get_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None, following=False): """ Return the list of all discussion threads pertaining to the given course @@ -223,8 +238,9 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te page_size: The number of threads to retrieve per page topic_id_list: The list of topic_ids to get the discussion threads for text_search A text search query string to match + following: If true, retrieve only threads the requester is following - Note that topic_id_list and text_search are mutually exclusive. + Note that topic_id_list, text_search, and following are mutually exclusive. Returns: @@ -238,15 +254,13 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te Http404: if the requesting user does not have access to the requested course or a page beyond the last is requested """ - exclusive_param_count = sum(1 for param in [topic_id_list, text_search] if param) + exclusive_param_count = sum(1 for param in [topic_id_list, text_search, following] if param) if exclusive_param_count > 1: # pragma: no cover raise ValueError("More than one mutually exclusive param passed to get_thread_list") course = _get_course_or_404(course_key, request.user) context = get_context(course, request) - topic_ids_csv = ",".join(topic_id_list) if topic_id_list else None - threads, result_page, num_pages, text_search_rewrite = Thread.search({ - "course_id": unicode(course.id), + query_params = { "group_id": ( None if context["is_requester_privileged"] else get_cohort_id(request.user, course.id) @@ -255,9 +269,16 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None, te "sort_order": "desc", "page": page, "per_page": page_size, - "commentable_ids": topic_ids_csv, "text": text_search, - }) + } + text_search_rewrite = None + if following: + threads, result_page, num_pages = context["cc_requester"].subscribed_threads(query_params) + else: + query_params["course_id"] = unicode(course.id) + query_params["commentable_ids"] = ",".join(topic_id_list) if topic_id_list else None + query_params["text"] = text_search + threads, result_page, num_pages, text_search_rewrite = Thread.search(query_params) # The comments service returns the last page of results if the requested # page is beyond the last page, but we want be consistent with DRF's general # behavior and return a 404 in that case @@ -368,6 +389,11 @@ def _do_extra_actions(api_content, cc_content, request_fields, actions_form, con context["cc_requester"].follow(cc_content) else: context["cc_requester"].unfollow(cc_content) + elif field == "abuse_flagged": + if form_value: + cc_content.flagAbuse(context["cc_requester"], cc_content) + else: + cc_content.unFlagAbuse(context["cc_requester"], cc_content, removeAll=False) else: assert field == "voted" if form_value: diff --git a/lms/djangoapps/discussion_api/forms.py b/lms/djangoapps/discussion_api/forms.py index 67a32b0f42..eaa9d69598 100644 --- a/lms/djangoapps/discussion_api/forms.py +++ b/lms/djangoapps/discussion_api/forms.py @@ -5,6 +5,7 @@ from django.core.exceptions import ValidationError from django.forms import ( BooleanField, CharField, + ChoiceField, Field, Form, IntegerField, @@ -45,11 +46,12 @@ class ThreadListGetForm(_PaginationForm): """ A form to validate query parameters in the thread list retrieval endpoint """ - EXCLUSIVE_PARAMS = ["topic_id", "text_search"] + EXCLUSIVE_PARAMS = ["topic_id", "text_search", "following"] course_id = CharField() topic_id = TopicIdField(required=False) text_search = CharField(required=False) + following = NullBooleanField(required=False) def clean_course_id(self): """Validate course_id""" @@ -59,6 +61,14 @@ class ThreadListGetForm(_PaginationForm): except InvalidKeyError: raise ValidationError("'{}' is not a valid course id".format(value)) + def clean_following(self): + """Validate following""" + value = self.cleaned_data["following"] + if value is False: + raise ValidationError("The value of the 'following' parameter must be true.") + else: + return value + def clean(self): cleaned_data = super(ThreadListGetForm, self).clean() exclusive_params_count = sum( @@ -80,6 +90,7 @@ class ThreadActionsForm(Form): """ following = BooleanField(required=False) voted = BooleanField(required=False) + abuse_flagged = BooleanField(required=False) class CommentListGetForm(_PaginationForm): @@ -98,3 +109,4 @@ class CommentActionsForm(Form): interactions with the comments service. """ voted = BooleanField(required=False) + abuse_flagged = BooleanField(required=False) diff --git a/lms/djangoapps/discussion_api/permissions.py b/lms/djangoapps/discussion_api/permissions.py index b76b58cbbe..596e3792f4 100644 --- a/lms/djangoapps/discussion_api/permissions.py +++ b/lms/djangoapps/discussion_api/permissions.py @@ -23,7 +23,7 @@ def get_editable_fields(cc_content, context): Return the set of fields that the requester can edit on the given content """ # Shared fields - ret = {"voted"} + ret = {"abuse_flagged", "voted"} if _is_author_or_privileged(cc_content, context): ret |= {"raw_body"} diff --git a/lms/djangoapps/discussion_api/serializers.py b/lms/djangoapps/discussion_api/serializers.py index f914e4e198..4ee207e16f 100644 --- a/lms/djangoapps/discussion_api/serializers.py +++ b/lms/djangoapps/discussion_api/serializers.py @@ -12,6 +12,7 @@ from rest_framework import serializers from discussion_api.permissions import get_editable_fields from discussion_api.render import render_body +from django_comment_client.utils import is_comment_too_deep from django_comment_common.models import ( FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_COMMUNITY_TA, @@ -46,6 +47,8 @@ def get_context(course, request, thread=None): for user in role.users.all() } requester = request.user + cc_requester = CommentClientUser.from_django_user(requester).retrieve() + cc_requester["course_id"] = course.id return { "course": course, "request": request, @@ -55,7 +58,7 @@ def get_context(course, request, thread=None): "is_requester_privileged": requester.id in staff_user_ids or requester.id in ta_user_ids, "staff_user_ids": staff_user_ids, "ta_user_ids": ta_user_ids, - "cc_requester": CommentClientUser.from_django_user(requester).retrieve(), + "cc_requester": cc_requester, } @@ -297,11 +300,12 @@ class CommentSerializer(_ContentSerializer): def validate(self, attrs): """ Ensure that parent_id identifies a comment that is actually in the - thread identified by thread_id. + thread identified by thread_id and does not violate the configured + maximum depth. """ + parent = None parent_id = attrs.get("parent_id") if parent_id: - parent = None try: parent = Comment(id=parent_id).retrieve() except CommentClientRequestError: @@ -310,6 +314,8 @@ class CommentSerializer(_ContentSerializer): raise ValidationError( "parent_id does not identify a comment in the thread identified by thread_id." ) + if is_comment_too_deep(parent): + raise ValidationError({"parent_id": ["Comment level is too deep."]}) return attrs def restore_object(self, attrs, instance=None): diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index e46a95e527..8286d2b0fa 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -99,6 +99,9 @@ class GetCourseTest(UrlResetMixin, ModuleStoreTestCase): "id": unicode(self.course.id), "blackouts": [], "thread_list_url": "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz", + "following_thread_list_url": ( + "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&following=True" + ), "topics_url": "http://testserver/api/discussion/v1/course_topics/x/y/z", } ) @@ -611,7 +614,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_0", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "voted"], + "editable_fields": ["abuse_flagged", "following", "voted"], }, { "id": "test_thread_id_1", @@ -642,7 +645,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "non_endorsed_comment_list_url": ( "http://testserver/api/discussion/v1/comments/?thread_id=test_thread_id_1&endorsed=False" ), - "editable_fields": ["following", "voted"], + "editable_fields": ["abuse_flagged", "following", "voted"], }, ] self.assertEqual( @@ -741,6 +744,31 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "text": ["test search string"], }) + def test_following(self): + self.register_subscribed_threads_response(self.user, [], page=1, num_pages=1) + result = get_thread_list( + self.request, + self.course.id, + page=1, + page_size=11, + following=True, + ) + self.assertEqual( + result, + {"results": [], "next": None, "previous": None, "text_search_rewrite": None} + ) + self.assertEqual( + urlparse(httpretty.last_request().path).path, + "/api/v1/users/{}/subscribed_threads".format(self.user.id) + ) + self.assert_last_query_params({ + "course_id": [unicode(self.course.id)], + "sort_key": ["date"], + "sort_order": ["desc"], + "page": ["1"], + "per_page": ["11"], + }) + @ddt.ddt class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): @@ -970,7 +998,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): "voted": False, "vote_count": 4, "children": [], - "editable_fields": ["voted"], + "editable_fields": ["abuse_flagged", "voted"], }, { "id": "test_comment_2", @@ -990,7 +1018,7 @@ class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): "voted": False, "vote_count": 7, "children": [], - "editable_fields": ["voted"], + "editable_fields": ["abuse_flagged", "voted"], }, ] actual_comments = self.get_comment_list( @@ -1210,7 +1238,7 @@ class CreateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_id", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"], + "editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"], } self.assertEqual(actual, expected) self.assertEqual( @@ -1278,6 +1306,18 @@ class CreateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC {"user_id": [str(self.user.id)], "value": ["up"]} ) + def test_abuse_flagged(self): + self.register_post_thread_response({"id": "test_id"}) + self.register_thread_flag_response("test_id") + data = self.minimal_data.copy() + data["abuse_flagged"] = "True" + result = create_thread(self.request, data) + self.assertEqual(result["abuse_flagged"], True) + cs_request = httpretty.last_request() + self.assertEqual(urlparse(cs_request.path).path, "/api/v1/threads/test_id/abuse_flag") + self.assertEqual(cs_request.method, "PUT") + self.assertEqual(cs_request.parsed_body, {"user_id": [str(self.user.id)]}) + def test_course_id_missing(self): with self.assertRaises(ValidationError) as assertion: create_thread(self.request, {}) @@ -1376,7 +1416,7 @@ class CreateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["raw_body", "voted"] + "editable_fields": ["abuse_flagged", "raw_body", "voted"] } self.assertEqual(actual, expected) expected_url = ( @@ -1431,6 +1471,18 @@ class CreateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest {"user_id": [str(self.user.id)], "value": ["up"]} ) + def test_abuse_flagged(self): + self.register_post_comment_response({"id": "test_comment"}, "test_thread") + self.register_comment_flag_response("test_comment") + data = self.minimal_data.copy() + data["abuse_flagged"] = "True" + result = create_comment(self.request, data) + self.assertEqual(result["abuse_flagged"], True) + cs_request = httpretty.last_request() + self.assertEqual(urlparse(cs_request.path).path, "/api/v1/comments/test_comment/abuse_flag") + self.assertEqual(cs_request.method, "PUT") + self.assertEqual(cs_request.parsed_body, {"user_id": [str(self.user.id)]}) + def test_thread_id_missing(self): with self.assertRaises(ValidationError) as assertion: create_comment(self.request, {}) @@ -1590,7 +1642,7 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"], + "editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"], } self.assertEqual(actual, expected) self.assertEqual( @@ -1770,6 +1822,41 @@ class UpdateThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestC expected_request_data["value"] = ["up"] self.assertEqual(actual_request_data, expected_request_data) + @ddt.data(*itertools.product([True, False], [True, False])) + @ddt.unpack + def test_abuse_flagged(self, old_flagged, new_flagged): + """ + Test attempts to edit the "abuse_flagged" field. + + old_flagged indicates whether the thread should be flagged at the start + of the test. new_flagged indicates the value for the "abuse_flagged" + field in the update. If old_flagged and new_flagged are the same, no + update should be made. Otherwise, a PUT should be made to the flag or + or unflag endpoint according to the new_flagged value. + """ + self.register_get_user_response(self.user) + self.register_thread_flag_response("test_thread") + self.register_thread({"abuse_flaggers": [str(self.user.id)] if old_flagged else []}) + data = {"abuse_flagged": new_flagged} + result = update_thread(self.request, "test_thread", data) + self.assertEqual(result["abuse_flagged"], new_flagged) + last_request_path = urlparse(httpretty.last_request().path).path + flag_url = "/api/v1/threads/test_thread/abuse_flag" + unflag_url = "/api/v1/threads/test_thread/abuse_unflag" + if old_flagged == new_flagged: + self.assertNotEqual(last_request_path, flag_url) + self.assertNotEqual(last_request_path, unflag_url) + else: + self.assertEqual( + last_request_path, + flag_url if new_flagged else unflag_url + ) + self.assertEqual(httpretty.last_request().method, "PUT") + self.assertEqual( + httpretty.last_request().parsed_body, + {"user_id": [str(self.user.id)]} + ) + def test_invalid_field(self): self.register_thread() with self.assertRaises(ValidationError) as assertion: @@ -1852,7 +1939,7 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["raw_body", "voted"] + "editable_fields": ["abuse_flagged", "raw_body", "voted"] } self.assertEqual(actual, expected) self.assertEqual( @@ -2038,6 +2125,41 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest expected_request_data["value"] = ["up"] self.assertEqual(actual_request_data, expected_request_data) + @ddt.data(*itertools.product([True, False], [True, False])) + @ddt.unpack + def test_abuse_flagged(self, old_flagged, new_flagged): + """ + Test attempts to edit the "abuse_flagged" field. + + old_flagged indicates whether the comment should be flagged at the start + of the test. new_flagged indicates the value for the "abuse_flagged" + field in the update. If old_flagged and new_flagged are the same, no + update should be made. Otherwise, a PUT should be made to the flag or + or unflag endpoint according to the new_flagged value. + """ + self.register_get_user_response(self.user) + self.register_comment_flag_response("test_comment") + self.register_comment({"abuse_flaggers": [str(self.user.id)] if old_flagged else []}) + data = {"abuse_flagged": new_flagged} + result = update_comment(self.request, "test_comment", data) + self.assertEqual(result["abuse_flagged"], new_flagged) + last_request_path = urlparse(httpretty.last_request().path).path + flag_url = "/api/v1/comments/test_comment/abuse_flag" + unflag_url = "/api/v1/comments/test_comment/abuse_unflag" + if old_flagged == new_flagged: + self.assertNotEqual(last_request_path, flag_url) + self.assertNotEqual(last_request_path, unflag_url) + else: + self.assertEqual( + last_request_path, + flag_url if new_flagged else unflag_url + ) + self.assertEqual(httpretty.last_request().method, "PUT") + self.assertEqual( + httpretty.last_request().parsed_body, + {"user_id": [str(self.user.id)]} + ) + @ddt.ddt class DeleteThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestCase): diff --git a/lms/djangoapps/discussion_api/tests/test_forms.py b/lms/djangoapps/discussion_api/tests/test_forms.py index 60223c58fd..1d3c8e87e5 100644 --- a/lms/djangoapps/discussion_api/tests/test_forms.py +++ b/lms/djangoapps/discussion_api/tests/test_forms.py @@ -94,6 +94,7 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): "page_size": 13, "topic_id": [], "text_search": "", + "following": None, } ) @@ -125,12 +126,20 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): self.form_data.setlist("topic_id", ["", "not empty"]) self.assert_error("topic_id", "This field cannot be empty.") - @ddt.data(*itertools.combinations(["topic_id", "text_search"], 2)) + def test_following_true(self): + self.form_data["following"] = "True" + self.assert_field_value("following", True) + + def test_following_false(self): + self.form_data["following"] = "False" + self.assert_error("following", "The value of the 'following' parameter must be true.") + + @ddt.data(*itertools.combinations(["topic_id", "text_search", "following"], 2)) def test_mutually_exclusive(self, params): - self.form_data.update({param: "dummy" for param in params}) + self.form_data.update({param: "True" for param in params}) self.assert_error( "__all__", - "The following query parameters are mutually exclusive: topic_id, text_search" + "The following query parameters are mutually exclusive: topic_id, text_search, following" ) diff --git a/lms/djangoapps/discussion_api/tests/test_permissions.py b/lms/djangoapps/discussion_api/tests/test_permissions.py index 66e8e84a8d..0de1c13d54 100644 --- a/lms/djangoapps/discussion_api/tests/test_permissions.py +++ b/lms/djangoapps/discussion_api/tests/test_permissions.py @@ -30,7 +30,7 @@ class GetEditableFieldsTest(TestCase): thread = Thread(user_id="5" if is_author else "6", type="thread") context = _get_context(requester_id="5", is_requester_privileged=is_privileged) actual = get_editable_fields(thread, context) - expected = {"following", "voted"} + expected = {"abuse_flagged", "following", "voted"} if is_author or is_privileged: expected |= {"topic_id", "type", "title", "raw_body"} self.assertEqual(actual, expected) @@ -45,7 +45,7 @@ class GetEditableFieldsTest(TestCase): thread=Thread(user_id="5" if is_thread_author else "6", thread_type=thread_type) ) actual = get_editable_fields(comment, context) - expected = {"voted"} + expected = {"abuse_flagged", "voted"} if is_author or is_privileged: expected |= {"raw_body"} if (is_thread_author and thread_type == "question") or is_privileged: diff --git a/lms/djangoapps/discussion_api/tests/test_serializers.py b/lms/djangoapps/discussion_api/tests/test_serializers.py index eac84ee1f3..7992715a30 100644 --- a/lms/djangoapps/discussion_api/tests/test_serializers.py +++ b/lms/djangoapps/discussion_api/tests/test_serializers.py @@ -197,7 +197,7 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "voted"], + "editable_fields": ["abuse_flagged", "following", "voted"], } self.assertEqual(self.serialize(thread), expected) @@ -305,7 +305,7 @@ class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase): "voted": False, "vote_count": 4, "children": [], - "editable_fields": ["voted"], + "editable_fields": ["abuse_flagged", "voted"], } self.assertEqual(self.serialize(comment), expected) @@ -670,6 +670,37 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore } ) + @ddt.data(None, -1, 0, 2, 5) + def test_create_parent_id_too_deep(self, max_depth): + with mock.patch("django_comment_client.utils.MAX_COMMENT_DEPTH", max_depth): + data = self.minimal_data.copy() + context = get_context(self.course, self.request, make_minimal_cs_thread()) + if max_depth is None or max_depth >= 0: + if max_depth != 0: + self.register_get_comment_response({ + "id": "not_too_deep", + "thread_id": "test_thread", + "depth": max_depth - 1 if max_depth else 100 + }) + data["parent_id"] = "not_too_deep" + else: + data["parent_id"] = None + serializer = CommentSerializer(data=data, context=context) + self.assertTrue(serializer.is_valid(), serializer.errors) + if max_depth is not None: + if max_depth >= 0: + self.register_get_comment_response({ + "id": "too_deep", + "thread_id": "test_thread", + "depth": max_depth + }) + data["parent_id"] = "too_deep" + else: + data["parent_id"] = None + serializer = CommentSerializer(data=data, context=context) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {"parent_id": ["Comment level is too deep."]}) + def test_create_missing_field(self): for field in self.minimal_data: data = self.minimal_data.copy() diff --git a/lms/djangoapps/discussion_api/tests/test_views.py b/lms/djangoapps/discussion_api/tests/test_views.py index 302badbc14..71807c9558 100644 --- a/lms/djangoapps/discussion_api/tests/test_views.py +++ b/lms/djangoapps/discussion_api/tests/test_views.py @@ -92,6 +92,9 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "id": unicode(self.course.id), "blackouts": [], "thread_list_url": "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz", + "following_thread_list_url": ( + "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&following=True" + ), "topics_url": "http://testserver/api/discussion/v1/course_topics/x/y/z", } ) @@ -204,7 +207,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "voted"], + "editable_fields": ["abuse_flagged", "following", "voted"], }] self.register_get_threads_response(source_threads, page=1, num_pages=2) response = self.client.get(self.url, {"course_id": unicode(self.course.id)}) @@ -270,6 +273,28 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "text": ["test search string"], }) + def test_following(self): + self.register_get_user_response(self.user) + self.register_subscribed_threads_response(self.user, [], page=1, num_pages=1) + response = self.client.get( + self.url, + { + "course_id": unicode(self.course.id), + "page": "1", + "page_size": "4", + "following": "True", + } + ) + self.assert_response_correct( + response, + 200, + {"results": [], "next": None, "previous": None, "text_search_rewrite": None} + ) + self.assertEqual( + urlparse(httpretty.last_request().path).path, + "/api/v1/users/{}/subscribed_threads".format(self.user.id) + ) + @httpretty.activate class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @@ -318,7 +343,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"], + "editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"], } response = self.client.post( self.url, @@ -409,7 +434,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None, - "editable_fields": ["following", "raw_body", "title", "topic_id", "type", "voted"], + "editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"], } response = self.client.patch( # pylint: disable=no-member self.url, @@ -553,7 +578,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "voted": True, "vote_count": 4, "children": [], - "editable_fields": ["voted"], + "editable_fields": ["abuse_flagged", "voted"], }] self.register_get_thread_response({ "id": self.thread_id, @@ -707,7 +732,7 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["raw_body", "voted"], + "editable_fields": ["abuse_flagged", "raw_body", "voted"], } response = self.client.post( self.url, @@ -791,7 +816,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes "voted": False, "vote_count": 0, "children": [], - "editable_fields": ["raw_body", "voted"], + "editable_fields": ["abuse_flagged", "raw_body", "voted"], } response = self.client.patch( # pylint: disable=no-member self.url, diff --git a/lms/djangoapps/discussion_api/tests/utils.py b/lms/djangoapps/discussion_api/tests/utils.py index 5dc0e3f5f5..61ed6974fa 100644 --- a/lms/djangoapps/discussion_api/tests/utils.py +++ b/lms/djangoapps/discussion_api/tests/utils.py @@ -191,6 +191,19 @@ class CommentsServiceMockMixin(object): status=200 ) + def register_subscribed_threads_response(self, user, threads, page, num_pages): + """Register a mock response for GET on the CS user instance endpoint""" + httpretty.register_uri( + httpretty.GET, + "http://localhost:4567/api/v1/users/{}/subscribed_threads".format(user.id), + body=json.dumps({ + "collection": threads, + "page": page, + "num_pages": num_pages, + }), + status=200 + ) + def register_subscription_response(self, user): """ Register a mock response for POST and DELETE on the CS user subscription @@ -230,6 +243,28 @@ class CommentsServiceMockMixin(object): status=200 ) + def register_flag_response(self, content_type, content_id): + """Register a mock response for PUT on the CS flag endpoints""" + for path in ["abuse_flag", "abuse_unflag"]: + httpretty.register_uri( + "PUT", + "http://localhost:4567/api/v1/{content_type}s/{content_id}/{path}".format( + content_type=content_type, + content_id=content_id, + path=path + ), + body=json.dumps({}), # body is unused + status=200 + ) + + def register_thread_flag_response(self, thread_id): + """Register a mock response for PUT on the CS thread flag endpoints""" + self.register_flag_response("thread", thread_id) + + def register_comment_flag_response(self, comment_id): + """Register a mock response for PUT on the CS comment flag endpoints""" + self.register_flag_response("comment", comment_id) + def register_delete_thread_response(self, thread_id): """ Register a mock response for DELETE on the CS thread instance endpoint diff --git a/lms/djangoapps/discussion_api/views.py b/lms/djangoapps/discussion_api/views.py index 0658bcf382..9002187fe6 100644 --- a/lms/djangoapps/discussion_api/views.py +++ b/lms/djangoapps/discussion_api/views.py @@ -141,6 +141,12 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): (including the bodies of comments in the thread) matches the search string will be returned. + * following: If true, retrieve only threads the requesting user is + following + + The topic_id, text_search, and following parameters are mutually + exclusive (i.e. only one may be specified in a request) + **POST Parameters**: * course_id (required): The course to create the thread in @@ -229,6 +235,7 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): form.cleaned_data["page_size"], form.cleaned_data["topic_id"], form.cleaned_data["text_search"], + form.cleaned_data["following"], ) ) diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 10e98655e1..bfe0cc7715 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -22,6 +22,7 @@ from django_comment_client.utils import ( add_courseware_context, get_annotated_content_info, get_ability, + is_comment_too_deep, JsonError, JsonResponse, prepare_content, @@ -313,9 +314,8 @@ def create_comment(request, course_id, thread_id): given a course_id and thread_id, test for comment depth. if not too deep, call _create_comment to create the actual comment. """ - if cc_settings.MAX_COMMENT_DEPTH is not None: - if cc_settings.MAX_COMMENT_DEPTH < 0: - return JsonError(_("Comment level too deep")) + if is_comment_too_deep(parent=None): + return JsonError(_("Comment level too deep")) return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread_id=thread_id) @@ -397,9 +397,8 @@ def create_sub_comment(request, course_id, comment_id): given a course_id and comment_id, create a response to a comment after checking the max depth allowed, if allowed """ - if cc_settings.MAX_COMMENT_DEPTH is not None: - if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth: - return JsonError(_("Comment level too deep")) + if is_comment_too_deep(parent=cc.Comment(comment_id)): + return JsonError(_("Comment level too deep")) return _create_comment(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), parent_id=comment_id) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 9c9d362f37..8608f1bce8 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -13,7 +13,7 @@ from django.core.context_processors import csrf from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.http import Http404, HttpResponseBadRequest -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_noop from django.views.decorators.http import require_GET import newrelic.agent @@ -55,10 +55,11 @@ class DiscussionTab(EnrolledTab): """ type = 'discussion' - title = _('Discussion') + title = ugettext_noop('Discussion') priority = None view_name = 'django_comment_client.forum.views.forum_form_discussion' is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False) + is_default = False @classmethod def is_enabled(cls, course, user=None): diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 153a3b2b12..7d8e2cf079 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -16,6 +16,7 @@ from xmodule.modulestore.django import modulestore from django_comment_common.models import Role, FORUM_ROLE_STUDENT from django_comment_client.permissions import check_permissions_by_view, has_permission +from django_comment_client.settings import MAX_COMMENT_DEPTH from edxmako import lookup_template from courseware.access import has_access @@ -568,3 +569,17 @@ def get_group_id_for_comments_service(request, course_key, commentable_id=None): # Never pass a group_id to the comments service for a non-cohorted # commentable return None + + +def is_comment_too_deep(parent): + """ + Determine whether a comment with the given parent violates MAX_COMMENT_DEPTH + + parent can be None to determine whether root comments are allowed + """ + return ( + MAX_COMMENT_DEPTH is not None and ( + MAX_COMMENT_DEPTH < 0 or + (parent and parent["depth"] >= MAX_COMMENT_DEPTH) + ) + ) diff --git a/lms/djangoapps/edxnotes/plugins.py b/lms/djangoapps/edxnotes/plugins.py index f427a3e9a2..0c0d3c559a 100644 --- a/lms/djangoapps/edxnotes/plugins.py +++ b/lms/djangoapps/edxnotes/plugins.py @@ -2,7 +2,7 @@ Registers the "edX Notes" feature for the edX platform. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_noop from courseware.tabs import EnrolledTab @@ -13,7 +13,7 @@ class EdxNotesTab(EnrolledTab): """ type = "edxnotes" - title = _("Notes") + title = ugettext_noop("Notes") view_name = "edxnotes" @classmethod diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 7e83dfe5a2..4adf82eddc 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -11,7 +11,7 @@ import pytz from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ugettext_noop from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from edxmako.shortcuts import render_to_response @@ -53,7 +53,7 @@ class InstructorDashboardTab(CourseTab): """ type = "instructor" - title = _('Instructor') + title = ugettext_noop('Instructor') view_name = "instructor_dashboard" is_dynamic = True # The "Instructor" tab is instead dynamically added when it is enabled diff --git a/lms/djangoapps/lti_provider/tests/test_users.py b/lms/djangoapps/lti_provider/tests/test_users.py index 508ab703ac..90025e3f98 100644 --- a/lms/djangoapps/lti_provider/tests/test_users.py +++ b/lms/djangoapps/lti_provider/tests/test_users.py @@ -5,6 +5,7 @@ Tests for the LTI user management functionality import string from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied from django.test import TestCase from django.test.client import RequestFactory from mock import patch, MagicMock @@ -25,21 +26,37 @@ class UserManagementHelperTest(TestCase): self.new_user = UserFactory.create() self.new_user.save() self.request.user = self.old_user + self.lti_consumer = LtiConsumer( + consumer_name='TestConsumer', + consumer_key='TestKey', + consumer_secret='TestSecret' + ) + self.lti_consumer.save() self.lti_user = LtiUser( lti_user_id='lti_user_id', edx_user=self.new_user ) - @patch('lti_provider.users.login') - def test_new_user_logged_in_by_switch_user(self, login_mock): - with patch('lti_provider.users.User.objects.get', return_value=self.new_user): - users.switch_user(self.request, self.lti_user) - login_mock.assert_called_with(self.request, self.new_user) + @patch('django.contrib.auth.authenticate', return_value=None) + def test_permission_denied_for_unknown_user(self, _authenticate_mock): + with self.assertRaises(PermissionDenied): + users.switch_user(self.request, self.lti_user, self.lti_consumer) @patch('lti_provider.users.login') - def test_backend_set_in_switch_user(self, _login_mock): - users.switch_user(self.request, self.lti_user) - self.assertIsNotNone(self.new_user.backend, 'Backend not set on user') + def test_authenticate_called(self, _login_mock): + with patch('lti_provider.users.authenticate', return_value=self.new_user) as authenticate: + users.switch_user(self.request, self.lti_user, self.lti_consumer) + authenticate.assert_called_with( + username=self.new_user.username, + lti_user_id=self.lti_user.lti_user_id, + lti_consumer=self.lti_consumer + ) + + @patch('lti_provider.users.login') + def test_login_called(self, login_mock): + with patch('lti_provider.users.authenticate', return_value=self.new_user): + users.switch_user(self.request, self.lti_user, self.lti_consumer) + login_mock.assert_called_with(self.request, self.new_user) def test_random_username_generator(self): for _idx in range(1000): @@ -93,7 +110,7 @@ class AuthenticateLtiUserTest(TestCase): with patch('lti_provider.users.create_lti_user', return_value=lti_user) as create_user: users.authenticate_lti_user(self.request, self.lti_user_id, self.lti_consumer) create_user.assert_called_with(self.lti_user_id, self.lti_consumer) - switch_user.assert_called_with(self.request, lti_user) + switch_user.assert_called_with(self.request, lti_user, self.lti_consumer) def test_authentication_with_authenticated_user(self, create_user, switch_user): lti_user = self.create_lti_user_model() @@ -109,7 +126,7 @@ class AuthenticateLtiUserTest(TestCase): self.request.user.is_authenticated = MagicMock(return_value=False) users.authenticate_lti_user(self.request, self.lti_user_id, self.lti_consumer) self.assertFalse(create_user.called) - switch_user.assert_called_with(self.request, lti_user) + switch_user.assert_called_with(self.request, lti_user, self.lti_consumer) def test_authentication_with_wrong_user(self, create_user, switch_user): lti_user = self.create_lti_user_model() @@ -117,7 +134,7 @@ class AuthenticateLtiUserTest(TestCase): self.request.user.is_authenticated = MagicMock(return_value=True) users.authenticate_lti_user(self.request, self.lti_user_id, self.lti_consumer) self.assertFalse(create_user.called) - switch_user.assert_called_with(self.request, lti_user) + switch_user.assert_called_with(self.request, lti_user, self.lti_consumer) class CreateLtiUserTest(TestCase): @@ -155,3 +172,71 @@ class CreateLtiUserTest(TestCase): self.assertEqual(User.objects.count(), 2) user = User.objects.get(username='new_edx_id') self.assertEqual(user.email, 'new_edx_id@lti.example.com') + + +class LtiBackendTest(TestCase): + """ + Tests for the authentication backend that authenticates LTI users. + """ + + def setUp(self): + super(LtiBackendTest, self).setUp() + self.edx_user = UserFactory.create() + self.edx_user.save() + self.lti_consumer = LtiConsumer( + consumer_key="Consumer Key", + consumer_secret="Consumer Secret" + ) + self.lti_consumer.save() + self.lti_user_id = 'LTI User ID' + LtiUser( + lti_consumer=self.lti_consumer, + lti_user_id=self.lti_user_id, + edx_user=self.edx_user + ).save() + + def test_valid_user_authenticates(self): + user = users.LtiBackend().authenticate( + username=self.edx_user.username, + lti_user_id=self.lti_user_id, + lti_consumer=self.lti_consumer + ) + self.assertEqual(user, self.edx_user) + + def test_missing_user_returns_none(self): + user = users.LtiBackend().authenticate( + username=self.edx_user.username, + lti_user_id='Invalid Username', + lti_consumer=self.lti_consumer + ) + self.assertIsNone(user) + + def test_non_lti_user_returns_none(self): + non_edx_user = UserFactory.create() + non_edx_user.save() + user = users.LtiBackend().authenticate( + username=non_edx_user.username, + ) + self.assertIsNone(user) + + def test_missing_lti_id_returns_null(self): + user = users.LtiBackend().authenticate( + username=self.edx_user.username, + lti_consumer=self.lti_consumer + ) + self.assertIsNone(user) + + def test_missing_lti_consumer_returns_null(self): + user = users.LtiBackend().authenticate( + username=self.edx_user.username, + lti_user_id=self.lti_user_id, + ) + self.assertIsNone(user) + + def test_existing_user_returned_by_get_user(self): + user = users.LtiBackend().get_user(self.edx_user.id) + self.assertEqual(user, self.edx_user) + + def test_get_user_returns_none_for_invalid_user(self): + user = users.LtiBackend().get_user(-1) + self.assertIsNone(user) diff --git a/lms/djangoapps/lti_provider/users.py b/lms/djangoapps/lti_provider/users.py index e6525707fa..dadaf8d7cc 100644 --- a/lms/djangoapps/lti_provider/users.py +++ b/lms/djangoapps/lti_provider/users.py @@ -7,11 +7,13 @@ import string import random import uuid -from django.contrib.auth import login +from django.conf import settings +from django.contrib.auth import authenticate, login from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied from django.db import IntegrityError - from lti_provider.models import LtiUser +from student.models import UserProfile def authenticate_lti_user(request, lti_user_id, lti_consumer): @@ -36,7 +38,7 @@ def authenticate_lti_user(request, lti_user_id, lti_consumer): request.user == lti_user.edx_user): # The user is not authenticated, or is logged in as somebody else. # Switch them to the LTI user - switch_user(request, lti_user) + switch_user(request, lti_user, lti_consumer) def create_lti_user(lti_user_id, lti_consumer): @@ -50,12 +52,17 @@ def create_lti_user(lti_user_id, lti_consumer): while not created: try: edx_user_id = generate_random_edx_username() + edx_email = "{}@{}".format(edx_user_id, settings.LTI_USER_EMAIL_DOMAIN) edx_user = User.objects.create_user( username=edx_user_id, password=edx_password, - email='{}@lti.example.com'.format(edx_user_id) + email=edx_email, ) - edx_user.save() + # A profile is required if PREVENT_CONCURRENT_LOGINS flag is set. + # TODO: We could populate user information from the LTI launch here, + # but it's not necessary for our current uses. + edx_user_profile = UserProfile(user=edx_user) + edx_user_profile.save() created = True except IntegrityError: # The random edx_user_id wasn't unique. Since 'created' is still @@ -71,14 +78,21 @@ def create_lti_user(lti_user_id, lti_consumer): return lti_user -def switch_user(request, lti_user): +def switch_user(request, lti_user, lti_consumer): """ Log out the current user, and log in using the edX identity associated with the LTI ID. """ - # The login function wants to know what backend authenticated the user. - lti_user.edx_user.backend = 'LTI_Provider' - login(request, lti_user.edx_user) + edx_user = authenticate( + username=lti_user.edx_user.username, + lti_user_id=lti_user.lti_user_id, + lti_consumer=lti_consumer + ) + if not edx_user: + # This shouldn't happen, since we've created edX accounts for any LTI + # users by this point, but just in case we can return a 403. + raise PermissionDenied() + login(request, edx_user) def generate_random_edx_username(): @@ -92,3 +106,46 @@ def generate_random_edx_username(): for _index in range(30): username = username + random.SystemRandom().choice(allowable_chars) return username + + +class LtiBackend(object): + """ + A Django authentication backend that authenticates users via LTI. This + backend will only return a User object if it is associated with an LTI + identity (i.e. the user was created by the create_lti_user method above). + """ + + def authenticate(self, username=None, lti_user_id=None, lti_consumer=None): + """ + Try to authenticate a user. This method will return a Django user object + if a user with the corresponding username exists in the database, and + if a record that links that user with an LTI user_id field exists in + the LtiUser collection. + + If such a user is not found, the method returns None (in line with the + authentication backend specification). + """ + try: + edx_user = User.objects.get(username=username) + except User.DoesNotExist: + return None + + try: + LtiUser.objects.get( + edx_user_id=edx_user.id, + lti_user_id=lti_user_id, + lti_consumer=lti_consumer + ) + except LtiUser.DoesNotExist: + return None + return edx_user + + def get_user(self, user_id): + """ + Return the User object for a user that has already been authenticated by + this backend. + """ + try: + return User.objects.get(id=user_id) + except User.DoesNotExist: + return None diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index 1674313343..b4876b3ae4 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -9,11 +9,11 @@ from student.models import CourseEnrollment, User from certificates.models import certificate_status_for_student, CertificateStatuses -class CourseField(serializers.RelatedField): +class CourseOverviewField(serializers.RelatedField): """Custom field to wrap a CourseDescriptor object. Read-only.""" - def to_native(self, course): - course_id = unicode(course.id) + def to_native(self, course_overview): + course_id = unicode(course_overview.id) request = self.context.get('request', None) if request: video_outline_url = reverse( @@ -38,14 +38,14 @@ class CourseField(serializers.RelatedField): return { "id": course_id, - "name": course.display_name, - "number": course.display_number_with_default, - "org": course.display_org_with_default, - "start": course.start, - "end": course.end, - "course_image": course_image_url(course), + "name": course_overview.display_name, + "number": course_overview.display_number_with_default, + "org": course_overview.display_org_with_default, + "start": course_overview.start, + "end": course_overview.end, + "course_image": course_overview.course_image_url, "social_urls": { - "facebook": course.facebook_url, + "facebook": course_overview.facebook_url, }, "latest_updates": { "video": None @@ -53,7 +53,7 @@ class CourseField(serializers.RelatedField): "video_outline": video_outline_url, "course_updates": course_updates_url, "course_handouts": course_handouts_url, - "subscription_id": course.clean_id(padding_char='_'), + "subscription_id": course_overview.clean_id(padding_char='_'), } @@ -61,7 +61,7 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer): """ Serializes CourseEnrollment models """ - course = CourseField() + course = CourseOverviewField(source="course_overview") certificate = serializers.SerializerMethodField('get_certificate') def get_certificate(self, model): diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 72e66cb0e5..19dae819c9 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -241,7 +241,8 @@ class UserCourseEnrollmentsList(generics.ListAPIView): ).order_by('created').reverse() return [ enrollment for enrollment in enrollments - if enrollment.course and is_mobile_available_for_user(self.request.user, enrollment.course) + if enrollment.course_overview and + is_mobile_available_for_user(self.request.user, enrollment.course_overview) ] diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index eedce977aa..4b2ce51c02 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -13,7 +13,7 @@ from courseware.tabs import EnrolledTab from notes.models import Note from notes.utils import notes_enabled_for_course from xmodule.annotator_token import retrieve_token -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_noop @login_required @@ -45,7 +45,7 @@ class NotesTab(EnrolledTab): A tab for the course notes. """ type = 'notes' - title = _("My Notes") + title = ugettext_noop("My Notes") view_name = "notes" @classmethod diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index d4a007d028..083cc602a9 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -1419,7 +1419,7 @@ class CouponRedemption(models.Model): """ return cls.objects.filter(order__status='purchased', coupon__course_id=course_id).values( 'coupon__code', 'coupon__percentage_discount' - ).annotate(coupon__used_count=Count('coupon__code')) + ).annotate(coupon__used_count=Count('coupon__code')).order_by('-coupon__used_count') @classmethod def get_total_coupon_code_purchases(cls, course_id): diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index eb7a797310..ab10d0bd7b 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -730,7 +730,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.login_user() resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=['non/existent/course'])) self.assertEqual(resp.status_code, 404) - self.assertIn(_("The course you requested does not exist."), resp.content) + self.assertIn("The course you requested does not exist.", resp.content) def test_add_course_to_cart_success(self): self.login_user() @@ -1924,7 +1924,7 @@ class CSVReportViewsTest(ModuleStoreTestCase): self.assertEqual(template, 'shoppingcart/download_report.html') self.assertFalse(context['total_count_error']) self.assertFalse(context['date_fmt_error']) - self.assertIn(_("Download CSV Reports"), response.content.decode('UTF-8')) + self.assertIn("Download CSV Reports", response.content.decode('UTF-8')) @patch('shoppingcart.views.render_to_response', render_mock) def test_report_csv_bad_date(self): @@ -1936,7 +1936,7 @@ class CSVReportViewsTest(ModuleStoreTestCase): self.assertEqual(template, 'shoppingcart/download_report.html') self.assertFalse(context['total_count_error']) self.assertTrue(context['date_fmt_error']) - self.assertIn(_("There was an error in your date input. It should be formatted as YYYY-MM-DD"), + self.assertIn("There was an error in your date input. It should be formatted as YYYY-MM-DD", response.content.decode('UTF-8')) CORRECT_CSV_NO_DATE_ITEMIZED_PURCHASE = ",1,purchased,1,40,40,usd,Registration for Course: Robot Super Course," diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index ed8a10aaae..786c3427b7 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -23,7 +23,7 @@ from openedx.core.djangoapps.user_api.accounts.api import activate_account, crea from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH from student.tests.factories import CourseModeFactory, UserFactory from student_account.views import account_settings_context -from third_party_auth.tests.testutil import simulate_running_pipeline +from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin from util.testing import UrlResetMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -204,7 +204,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase): @ddt.ddt -class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase): +class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleStoreTestCase): """ Tests for the student account views that update the user's account information. """ USERNAME = "bob" @@ -214,6 +214,9 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) @mock.patch.dict(settings.FEATURES, {'EMBARGO': True}) def setUp(self): super(StudentAccountLoginAndRegistrationTest, self).setUp('embargo') + # For these tests, two third party auth providers are enabled by default: + self.configure_google_provider(enabled=True) + self.configure_facebook_provider(enabled=True) @ddt.data( ("account_login", "login"), @@ -239,7 +242,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) @ddt.data( (False, "account_login"), - (False, "account_login"), + (False, "account_register"), (True, "account_login"), (True, "account_register"), ) @@ -290,7 +293,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) @ddt.unpack def test_third_party_auth(self, url_name, current_backend, current_provider): params = [ - ('course_id', 'edX/DemoX/Demo_Course'), + ('course_id', 'course-v1:Org+Course+Run'), ('enrollment_action', 'enroll'), ('course_mode', 'honor'), ('email_opt_in', 'true'), @@ -310,12 +313,14 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) # This relies on the THIRD_PARTY_AUTH configuration in the test settings expected_providers = [ { + "id": "oa2-facebook", "name": "Facebook", "iconClass": "fa-facebook", "loginUrl": self._third_party_login_url("facebook", "login", params), "registerUrl": self._third_party_login_url("facebook", "register", params) }, { + "id": "oa2-google-oauth2", "name": "Google", "iconClass": "fa-google-plus", "loginUrl": self._third_party_login_url("google-oauth2", "login", params), @@ -324,6 +329,11 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) ] self._assert_third_party_auth_data(response, current_backend, current_provider, expected_providers) + def test_hinted_login(self): + params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] + response = self.client.get(reverse('account_login'), params) + self.assertContains(response, "data-third-party-auth-hint='oa2-google-oauth2'") + @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) def test_microsite_uses_old_login_page(self): # Retrieve the login page from a microsite domain @@ -347,11 +357,15 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) def _assert_third_party_auth_data(self, response, current_backend, current_provider, providers): """Verify that third party auth info is rendered correctly in a DOM data attribute. """ + finish_auth_url = None + if current_backend: + finish_auth_url = reverse("social:complete", kwargs={"backend": current_backend}) + "?" auth_info = markupsafe.escape( json.dumps({ "currentProvider": current_provider, "providers": providers, - "finishAuthUrl": "/auth/complete/{}?".format(current_backend) if current_backend else None, + "secondaryProviders": [], + "finishAuthUrl": finish_auth_url, "errorMessage": None, }) ) @@ -382,7 +396,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) }) -class AccountSettingsViewTest(TestCase): +class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase): """ Tests for the account settings view. """ USERNAME = 'student' @@ -406,6 +420,10 @@ class AccountSettingsViewTest(TestCase): self.request = RequestFactory() self.request.user = self.user + # For these tests, two third party auth providers are enabled by default: + self.configure_google_provider(enabled=True) + self.configure_facebook_provider(enabled=True) + # Python-social saves auth failure notifcations in Django messages. # See pipeline.get_duplicate_provider() for details. self.request.COOKIES = {} @@ -432,7 +450,7 @@ class AccountSettingsViewTest(TestCase): context['user_preferences_api_url'], reverse('preferences_api', kwargs={'username': self.user.username}) ) - self.assertEqual(context['duplicate_provider'].BACKEND_CLASS.name, 'facebook') + self.assertEqual(context['duplicate_provider'], 'facebook') self.assertEqual(context['auth']['providers'][0]['name'], 'Facebook') self.assertEqual(context['auth']['providers'][1]['name'], 'Google') diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index dc178285ff..76602b1337 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -2,6 +2,7 @@ import logging import json +import urlparse from django.conf import settings from django.contrib import messages @@ -77,12 +78,26 @@ def login_and_registration_form(request, initial_mode="login"): if ext_auth_response is not None: return ext_auth_response + # Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check. + # If present, we display a login page focused on third-party auth with that provider. + third_party_auth_hint = None + if '?' in redirect_to: + try: + next_args = urlparse.parse_qs(urlparse.urlparse(redirect_to).query) + provider_id = next_args['tpa_hint'][0] + if third_party_auth.provider.Registry.get(provider_id=provider_id): + third_party_auth_hint = provider_id + initial_mode = "hinted_login" + except (KeyError, ValueError, IndexError): + pass + # Otherwise, render the combined login/registration page context = { 'login_redirect_url': redirect_to, # This gets added to the query string of the "Sign In" button in the header 'disable_courseware_js': True, 'initial_mode': initial_mode, 'third_party_auth': json.dumps(_third_party_auth_context(request, redirect_to)), + 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': settings.PLATFORM_NAME, 'responsive': True, @@ -164,41 +179,45 @@ def _third_party_auth_context(request, redirect_to): context = { "currentProvider": None, "providers": [], + "secondaryProviders": [], "finishAuthUrl": None, "errorMessage": None, } if third_party_auth.is_enabled(): - context["providers"] = [ - { - "name": enabled.NAME, - "iconClass": enabled.ICON_CLASS, + for enabled in third_party_auth.provider.Registry.enabled(): + info = { + "id": enabled.provider_id, + "name": enabled.name, + "iconClass": enabled.icon_class, "loginUrl": pipeline.get_login_url( - enabled.NAME, + enabled.provider_id, pipeline.AUTH_ENTRY_LOGIN, redirect_url=redirect_to, ), "registerUrl": pipeline.get_login_url( - enabled.NAME, + enabled.provider_id, pipeline.AUTH_ENTRY_REGISTER, redirect_url=redirect_to, ), } - for enabled in third_party_auth.provider.Registry.enabled() - ] + context["providers" if not enabled.secondary else "secondaryProviders"].append(info) running_pipeline = pipeline.get(request) if running_pipeline is not None: - current_provider = third_party_auth.provider.Registry.get_by_backend_name( - running_pipeline.get('backend') - ) - context["currentProvider"] = current_provider.NAME - context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.BACKEND_CLASS.name) + current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) + context["currentProvider"] = current_provider.name + context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) + + if current_provider.skip_registration_form: + # As a reliable way of "skipping" the registration form, we just submit it automatically + context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): if msg.extra_tags.split()[0] == "social-auth": - context['errorMessage'] = unicode(msg) + # msg may or may not be translated. Try translating [again] in case we are able to: + context['errorMessage'] = _(unicode(msg)) # pylint: disable=translation-of-non-string break return context @@ -370,19 +389,20 @@ def account_settings_context(request): auth_states = pipeline.get_provider_user_states(user) context['auth']['providers'] = [{ - 'name': state.provider.NAME, # The name of the provider e.g. Facebook + 'id': state.provider.provider_id, + 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate # with the particular provider. 'connect_url': pipeline.get_login_url( - state.provider.NAME, + state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. - 'disconnect_url': pipeline.get_disconnect_url(state.provider.NAME), + 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), } for state in auth_states] return context diff --git a/lms/djangoapps/teams/plugins.py b/lms/djangoapps/teams/plugins.py index 9ff162288e..a613eb7740 100644 --- a/lms/djangoapps/teams/plugins.py +++ b/lms/djangoapps/teams/plugins.py @@ -2,7 +2,7 @@ Definition of the course team feature. """ -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_noop from courseware.tabs import EnrolledTab from .views import is_feature_enabled @@ -13,7 +13,7 @@ class TeamsTab(EnrolledTab): """ type = "teams" - title = _("Teams") + title = ugettext_noop("Teams") view_name = "teams_dashboard" @classmethod diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 833b8e4b95..c97593fe84 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -41,9 +41,8 @@ from student.models import CourseEnrollment from util.date_utils import get_default_time_display from util.testing import UrlResetMixin from verify_student.views import ( - checkout_with_ecommerce_service, - render_to_response, PayAndVerifyView, EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, - EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, _send_email, _compose_message_reverification_email + checkout_with_ecommerce_service, render_to_response, PayAndVerifyView, + _send_email, _compose_message_reverification_email ) from verify_student.models import ( SoftwareSecurePhotoVerification, VerificationCheckpoint, @@ -1584,52 +1583,84 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): VerificationStatus.add_verification_status(checkpoint, self.user, "submitted") -class TestReverifyView(ModuleStoreTestCase): +class TestReverifyView(TestCase): """ - Tests for the reverification views. + Tests for the reverification view. + + Reverification occurs when a verification attempt is denied or expired, + and the student is given the option to resubmit. """ + + USERNAME = "shaftoe" + PASSWORD = "detachment-2702" + def setUp(self): super(TestReverifyView, self).setUp() + self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) + success = self.client.login(username=self.USERNAME, password=self.PASSWORD) + self.assertTrue(success, msg="Could not log in") - self.user = UserFactory.create(username="rusty", password="test") - self.user.profile.name = u"Røøsty Bøøgins" - self.user.profile.save() - self.client.login(username="rusty", password="test") - self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_key = self.course.id + def test_reverify_view_can_reverify_denied(self): + # User has a denied attempt, so can reverify + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + attempt.deny("error") + self._assert_can_reverify() - @patch('verify_student.views.render_to_response', render_mock) - def test_reverify_get(self): - url = reverse('verify_student_reverify') - response = self.client.get(url) - self.assertEquals(response.status_code, 200) - ((_template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence - self.assertFalse(context['error']) + def test_reverify_view_can_reverify_expired(self): + # User has a verification attempt, but it's expired + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + attempt.approve() - @patch('verify_student.views.render_to_response', render_mock) - def test_reverify_post_failure(self): - url = reverse('verify_student_reverify') - response = self.client.post(url, {'face_image': '', - 'photo_id_image': ''}) - self.assertEquals(response.status_code, 200) - ((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence - self.assertIn('photo_reverification', template) - self.assertTrue(context['error']) + days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] + attempt.created_at = datetime.now(pytz.UTC) - timedelta(days=(days_good_for + 1)) + attempt.save() - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_reverify_post_success(self): - url = reverse('verify_student_reverify') - response = self.client.post(url, {'face_image': ',', - 'photo_id_image': ','}) - self.assertEquals(response.status_code, 302) - try: - verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) - self.assertIsNotNone(verification_attempt) - except ObjectDoesNotExist: - self.fail('No verification object generated') - ((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence - self.assertIn('photo_reverification', template) - self.assertTrue(context['error']) + # Allow the student to reverify + self._assert_can_reverify() + + def test_reverify_view_cannot_reverify_pending(self): + # User has submitted a verification attempt, but Software Secure has not yet responded + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + + # Cannot reverify because an attempt has already been submitted. + self._assert_cannot_reverify() + + def test_reverify_view_cannot_reverify_approved(self): + # Submitted attempt has been approved + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + attempt.approve() + + # Cannot reverify because the user is already verified. + self._assert_cannot_reverify() + + def _get_reverify_page(self): + """ + Retrieve the reverification page and return the response. + """ + url = reverse("verify_student_reverify") + return self.client.get(url) + + def _assert_can_reverify(self): + """ + Check that the reverification flow is rendered. + """ + response = self._get_reverify_page() + self.assertContains(response, "reverify-container") + + def _assert_cannot_reverify(self): + """ + Check that the user is blocked from reverifying. + """ + response = self._get_reverify_page() + self.assertContains(response, "reverify-blocked") class TestInCourseReverifyView(ModuleStoreTestCase): @@ -1727,7 +1758,7 @@ class TestInCourseReverifyView(ModuleStoreTestCase): # submitting the photo verification self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member self.user.id, # pylint: disable=no-member - EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, + 'edx.bi.reverify.started', { 'category': "verification", 'label': unicode(self.course_key), @@ -1781,7 +1812,7 @@ class TestInCourseReverifyView(ModuleStoreTestCase): # photo verification self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member self.user.id, - EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, + 'edx.bi.reverify.submitted', { 'category': "verification", 'label': unicode(self.course_key), @@ -1934,7 +1965,7 @@ class TestEmailMessageWithCustomICRVBlock(ModuleStoreTestCase): "We could not verify your identity for the {assessment} assessment " "in the {course_name} course. You have used " "{used_attempts} out of {allowed_attempts} attempts to " - "verify your identity.".format( + "verify your identity".format( course_name=self.course.display_name_with_default, assessment=self.assessment, used_attempts=1, @@ -1992,8 +2023,7 @@ class TestEmailMessageWithCustomICRVBlock(ModuleStoreTestCase): def test_denied_email_message_with_close_verification_dates(self): # Due date given and expired - - return_value = datetime(2016, 1, 1, tzinfo=timezone.utc) + return_value = datetime.now(tz=pytz.UTC) + timedelta(days=22) with patch.object(timezone, 'now', return_value=return_value): __, body = _compose_message_reverification_email( self.course.id, self.user.id, self.reverification_location, "denied", self.request diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index 59b907f7c2..05e4926ea1 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -89,29 +89,24 @@ urlpatterns = patterns( name="verify_student_results_callback", ), + url( + r'^submit-photos/$', + views.submit_photos_for_verification, + name="verify_student_submit_photos" + ), + + # End-point for reverification + # Reverification occurs when a user's initial verification attempt + # is denied or expires. The user is allowed to retry by submitting + # new photos. This is different than *in-course* reverification, + # in which a student submits only face photos, which are matched + # against the ID photo from the user's initial verification attempt. url( r'^reverify$', views.ReverifyView.as_view(), name="verify_student_reverify" ), - url( - r'^reverification_confirmation$', - views.reverification_submission_confirmation, - name="verify_student_reverification_confirmation" - ), - - url( - r'^reverification_window_expired$', - views.reverification_window_expired, - name="verify_student_reverification_window_expired" - ), - - url( - r'^submit-photos/$', - views.submit_photos_for_verification, - name="verify_student_submit_photos" - ), # Endpoint for in-course reverification # Users are sent to this end-point from within courseware # to re-verify their identities by re-submitting face photos. diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 967759346d..173c7152d2 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -6,7 +6,6 @@ import datetime import decimal import json import logging -from collections import namedtuple from pytz import UTC from ipware.ip import get_ip @@ -14,10 +13,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.mail import send_mail from django.core.urlresolvers import reverse -from django.http import ( - HttpResponse, HttpResponseBadRequest, - HttpResponseRedirect, Http404 -) +from django.http import HttpResponse, HttpResponseBadRequest, Http404 from django.contrib.auth.models import User from django.shortcuts import redirect from django.utils import timezone @@ -42,7 +38,7 @@ from microsite_configuration import microsite from openedx.core.djangoapps.user_api.accounts import NAME_MIN_LENGTH from openedx.core.djangoapps.user_api.accounts.api import get_account_settings, update_account_settings from openedx.core.djangoapps.user_api.errors import UserNotFound, AccountValidationError -from openedx.core.djangoapps.credit.api import get_credit_requirement, set_credit_requirement_status +from openedx.core.djangoapps.credit.api import set_credit_requirement_status from student.models import CourseEnrollment from shoppingcart.models import Order, CertificateItem from shoppingcart.processors import ( @@ -63,8 +59,6 @@ from staticfiles.storage import staticfiles_storage log = logging.getLogger(__name__) -EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW = 'edx.bi.reverify.started' -EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY = 'edx.bi.reverify.submitted' class PayAndVerifyView(View): @@ -156,43 +150,14 @@ class PayAndVerifyView(View): INTRO_STEP, ] - Step = namedtuple( - 'Step', - [ - 'title', - 'template_name' - ] - ) - - STEP_INFO = { - INTRO_STEP: Step( - title=ugettext_lazy("Intro"), - template_name="intro_step" - ), - MAKE_PAYMENT_STEP: Step( - title=ugettext_lazy("Make payment"), - template_name="make_payment_step" - ), - PAYMENT_CONFIRMATION_STEP: Step( - title=ugettext_lazy("Payment confirmation"), - template_name="payment_confirmation_step" - ), - FACE_PHOTO_STEP: Step( - title=ugettext_lazy("Take photo"), - template_name="face_photo_step" - ), - ID_PHOTO_STEP: Step( - title=ugettext_lazy("Take a photo of your ID"), - template_name="id_photo_step" - ), - REVIEW_PHOTOS_STEP: Step( - title=ugettext_lazy("Review your info"), - template_name="review_photos_step" - ), - ENROLLMENT_CONFIRMATION_STEP: Step( - title=ugettext_lazy("Enrollment confirmation"), - template_name="enrollment_confirmation_step" - ), + STEP_TITLES = { + INTRO_STEP: ugettext_lazy("Intro"), + MAKE_PAYMENT_STEP: ugettext_lazy("Make payment"), + PAYMENT_CONFIRMATION_STEP: ugettext_lazy("Payment confirmation"), + FACE_PHOTO_STEP: ugettext_lazy("Take photo"), + ID_PHOTO_STEP: ugettext_lazy("Take a photo of your ID"), + REVIEW_PHOTOS_STEP: ugettext_lazy("Review your info"), + ENROLLMENT_CONFIRMATION_STEP: ugettext_lazy("Enrollment confirmation"), } # Messages @@ -554,8 +519,7 @@ class PayAndVerifyView(View): return [ { 'name': step, - 'title': unicode(self.STEP_INFO[step].title), - 'templateName': self.STEP_INFO[step].template_name + 'title': unicode(self.STEP_TITLES[step]), } for step in display_steps if step not in remove_steps @@ -933,19 +897,19 @@ def _set_user_requirement_status(attempt, namespace, status, reason=None): log.error("Unable to find checkpoint for user with id %d", attempt.user.id) if checkpoint is not None: - course_key = checkpoint.course_id - credit_requirement = get_credit_requirement( - course_key, namespace, checkpoint.checkpoint_location - ) - if credit_requirement is not None: - try: - set_credit_requirement_status( - attempt.user.username, credit_requirement, status, reason - ) - except Exception: # pylint: disable=broad-except - # Catch exception if unable to add credit requirement - # status for user - log.error("Unable to add Credit requirement status for user with id %d", attempt.user.id) + try: + set_credit_requirement_status( + attempt.user.username, + checkpoint.course_id, + namespace, + checkpoint.checkpoint_location, + status=status, + reason=reason, + ) + except Exception: # pylint: disable=broad-except + # Catch exception if unable to add credit requirement + # status for user + log.error("Unable to add Credit requirement status for user with id %d", attempt.user.id) @require_POST @@ -1044,85 +1008,52 @@ def results_callback(request): class ReverifyView(View): """ - The main reverification view. Under similar constraints as the main verification view. - Has to perform these functions: - - take new face photo - - take new id photo - - submit photos to photo verification service + Reverification occurs when a user's initial verification is denied + or expires. When this happens, users can re-submit photos through + the re-verification flow. + + Unlike in-course reverification, this flow requires users to submit + *both* face and ID photos. In contrast, during in-course reverification, + students submit only face photos, which are matched against the ID photo + the user submitted during initial verification. - Does not need to be attached to a particular course. - Does not need to worry about pricing """ @method_decorator(login_required) def get(self, request): """ - display this view + Render the reverification flow. + + Most of the work is done client-side by composing the same + Backbone views used in the initial verification flow. """ - context = { - "user_full_name": request.user.profile.name, - "error": False, - } - - return render_to_response("verify_student/photo_reverification.html", context) - - @method_decorator(login_required) - def post(self, request): - """ - submits the reverification to SoftwareSecure - """ - - try: - attempt = SoftwareSecurePhotoVerification(user=request.user) - b64_face_image = request.POST['face_image'].split(",")[1] - b64_photo_id_image = request.POST['photo_id_image'].split(",")[1] - - attempt.upload_face_image(b64_face_image.decode('base64')) - attempt.upload_photo_id_image(b64_photo_id_image.decode('base64')) - attempt.mark_ready() - - # save this attempt - attempt.save() - # then submit it across - attempt.submit() - return HttpResponseRedirect(reverse('verify_student_reverification_confirmation')) - except Exception: - log.exception( - "Could not submit verification attempt for user {}".format(request.user.id) - ) + status, _ = SoftwareSecurePhotoVerification.user_status(request.user) + if status in ["must_reverify", "expired"]: context = { "user_full_name": request.user.profile.name, - "error": True, + "platform_name": settings.PLATFORM_NAME, + "capture_sound": staticfiles_storage.url("audio/camera_capture.wav"), } - return render_to_response("verify_student/photo_reverification.html", context) - - -@login_required -def reverification_submission_confirmation(_request): - """ - Shows the user a confirmation page if the submission to SoftwareSecure was successful - """ - return render_to_response("verify_student/reverification_confirmation.html") - - -@login_required -def reverification_window_expired(_request): - """ - Displays an error page if a student tries to submit a reverification, but the window - for that reverification has already expired. - """ - # TODO need someone to review the copy for this template - return render_to_response("verify_student/reverification_window_expired.html") + return render_to_response("verify_student/reverify.html", context) + else: + context = { + "status": status + } + return render_to_response("verify_student/reverify_not_allowed.html", context) class InCourseReverifyView(View): """ The in-course reverification view. - Needs to perform these functions: - - take new face photo - - retrieve the old id photo - - submit these photos to photo verification service - Does not need to worry about pricing + In-course reverification occurs while a student is taking a course. + At points in the course, students are prompted to submit face photos, + which are matched against the ID photos the user submitted during their + initial verification. + + Students are prompted to enter this flow from an "In Course Reverification" + XBlock (courseware component) that course authors add to the course. + See https://github.com/edx/edx-reverification-block for more details. + """ @method_decorator(login_required) def get(self, request, course_id, usage_id): @@ -1168,9 +1099,7 @@ class InCourseReverifyView(View): return self._redirect_no_initial_verification(user, course_key) # emit the reverification event - self._track_reverification_events( - EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, user.id, course_id, checkpoint.checkpoint_name - ) + self._track_reverification_events('edx.bi.reverify.started', user.id, course_id, checkpoint.checkpoint_name) context = { 'course_key': unicode(course_key), @@ -1235,7 +1164,8 @@ class InCourseReverifyView(View): # emit the reverification event self._track_reverification_events( - EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, user.id, course_id, checkpoint.checkpoint_name + 'edx.bi.reverify.submitted', + user.id, course_id, checkpoint.checkpoint_name ) redirect_url = get_redirect_url(course_key, usage_key) diff --git a/lms/djangoapps/xblock_user_state/interface.py b/lms/djangoapps/xblock_user_state/interface.py deleted file mode 100644 index 2c7254d00f..0000000000 --- a/lms/djangoapps/xblock_user_state/interface.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -A baseclass for a generic client for accessing XBlock Scope.user_state field data. -""" - -from abc import abstractmethod - -from contracts import contract, new_contract, ContractsMeta -from opaque_keys.edx.keys import UsageKey -from xblock.fields import Scope, ScopeBase - -new_contract('UsageKey', UsageKey) - - -class XBlockUserStateClient(object): - """ - First stab at an interface for accessing XBlock User State. This will have - use StudentModule as a backing store in the default case. - - Scope/Goals: - 1. Mediate access to all student-specific state stored by XBlocks. - a. This includes "preferences" and "user_info" (i.e. UserScope.ONE) - b. This includes XBlock Asides. - c. This may later include user_state_summary (i.e. UserScope.ALL). - d. This may include group state in the future. - e. This may include other key types + UserScope.ONE (e.g. Definition) - 2. Assume network service semantics. - At some point, this will probably be calling out to an external service. - Even if it doesn't, we want to be able to implement circuit breakers, so - that a failure in StudentModule doesn't bring down the whole site. - This also implies that the client is running as a user, and whatever is - backing it is smart enough to do authorization checks. - 3. This does not yet cover export-related functionality. - - Open Questions: - 1. Is it sufficient to just send the block_key in and extract course + - version info from it? - 2. Do we want to use the username as the identifier? Privacy implications? - Ease of debugging? - 3. Would a get_many_by_type() be useful? - """ - - __metaclass__ = ContractsMeta - - class ServiceUnavailable(Exception): - """ - This error is raised if the service backing this client is currently unavailable. - """ - pass - - class PermissionDenied(Exception): - """ - This error is raised if the caller is not allowed to access the requested data. - """ - pass - - class DoesNotExist(Exception): - """ - This error is raised if the caller has requested data that does not exist. - """ - pass - - @contract( - username="basestring", - block_key=UsageKey, - scope=ScopeBase, - fields="seq(basestring)|set(basestring)|None", - returns="dict(basestring: *)" - ) - def get(self, username, block_key, scope=Scope.user_state, fields=None): - """ - Retrieve the stored XBlock state for a single xblock usage. - - Arguments: - username: The name of the user whose state should be retrieved - block_key (UsageKey): The UsageKey identifying which xblock state to load. - scope (Scope): The scope to load data from - fields: A list of field values to retrieve. If None, retrieve all stored fields. - - Returns - dict: A dictionary mapping field names to values - """ - return next(self.get_many(username, [block_key], scope, fields=fields))[1] - - @contract( - username="basestring", - block_key=UsageKey, - state="dict(basestring: *)", - scope=ScopeBase, - returns=None, - ) - def set(self, username, block_key, state, scope=Scope.user_state): - """ - Set fields for a particular XBlock. - - Arguments: - username: The name of the user whose state should be retrieved - block_key (UsageKey): The UsageKey identifying which xblock state to load. - state (dict): A dictionary mapping field names to values - scope (Scope): The scope to store data to - """ - self.set_many(username, {block_key: state}, scope) - - @contract( - username="basestring", - block_key=UsageKey, - scope=ScopeBase, - fields="seq(basestring)|set(basestring)|None", - returns=None, - ) - def delete(self, username, block_key, scope=Scope.user_state, fields=None): - """ - Delete the stored XBlock state for a single xblock usage. - - Arguments: - username: The name of the user whose state should be deleted - block_key (UsageKey): The UsageKey identifying which xblock state to delete. - scope (Scope): The scope to delete data from - fields: A list of fields to delete. If None, delete all stored fields. - """ - return self.delete_many(username, [block_key], scope, fields=fields) - - @contract( - username="basestring", - block_key=UsageKey, - scope=ScopeBase, - fields="seq(basestring)|set(basestring)|None", - returns="dict(basestring: datetime)", - ) - def get_mod_date(self, username, block_key, scope=Scope.user_state, fields=None): - """ - Get the last modification date for fields from the specified blocks. - - Arguments: - username: The name of the user whose state should queried - block_key (UsageKey): The UsageKey identifying which xblock modification dates to retrieve. - scope (Scope): The scope to retrieve from. - fields: A list of fields to query. If None, query all fields. - Specific implementations are free to return the same modification date - for all fields, if they don't store changes individually per field. - Implementations may omit fields for which data has not been stored. - - Returns: list a dict of {field_name: modified_date} for each selected field. - """ - results = self.get_mod_date_many(username, [block_key], scope, fields=fields) - return { - field: date for (_, field, date) in results - } - - @contract( - username="basestring", - block_keys="seq(UsageKey)|set(UsageKey)", - scope=ScopeBase, - fields="seq(basestring)|set(basestring)|None", - ) - @abstractmethod - def get_many(self, username, block_keys, scope=Scope.user_state, fields=None): - """ - Retrieve the stored XBlock state for a single xblock usage. - - Arguments: - username: The name of the user whose state should be retrieved - block_keys ([UsageKey]): A list of UsageKeys identifying which xblock states to load. - scope (Scope): The scope to load data from - fields: A list of field values to retrieve. If None, retrieve all stored fields. - - Yields: - (UsageKey, field_state) tuples for each specified UsageKey in block_keys. - field_state is a dict mapping field names to values. - """ - raise NotImplementedError() - - @contract( - username="basestring", - block_keys_to_state="dict(UsageKey: dict(basestring: *))", - scope=ScopeBase, - returns=None, - ) - @abstractmethod - def set_many(self, username, block_keys_to_state, scope=Scope.user_state): - """ - Set fields for a particular XBlock. - - Arguments: - username: The name of the user whose state should be retrieved - block_keys_to_state (dict): A dict mapping UsageKeys to state dicts. - Each state dict maps field names to values. These state dicts - are overlaid over the stored state. To delete fields, use - :meth:`delete` or :meth:`delete_many`. - scope (Scope): The scope to load data from - """ - raise NotImplementedError() - - @contract( - username="basestring", - block_keys="seq(UsageKey)|set(UsageKey)", - scope=ScopeBase, - fields="seq(basestring)|set(basestring)|None", - returns=None, - ) - @abstractmethod - def delete_many(self, username, block_keys, scope=Scope.user_state, fields=None): - """ - Delete the stored XBlock state for a many xblock usages. - - Arguments: - username: The name of the user whose state should be deleted - block_key (UsageKey): The UsageKey identifying which xblock state to delete. - scope (Scope): The scope to delete data from - fields: A list of fields to delete. If None, delete all stored fields. - """ - raise NotImplementedError() - - @contract( - username="basestring", - block_keys="seq(UsageKey)|set(UsageKey)", - scope=ScopeBase, - fields="seq(basestring)|set(basestring)|None", - ) - @abstractmethod - def get_mod_date_many(self, username, block_keys, scope=Scope.user_state, fields=None): - """ - Get the last modification date for fields from the specified blocks. - - Arguments: - username: The name of the user whose state should be queried - block_key (UsageKey): The UsageKey identifying which xblock modification dates to retrieve. - scope (Scope): The scope to retrieve from. - fields: A list of fields to query. If None, delete all stored fields. - Specific implementations are free to return the same modification date - for all fields, if they don't store changes individually per field. - Implementations may omit fields for which data has not been stored. - - Yields: tuples of (block, field_name, modified_date) for each selected field. - """ - raise NotImplementedError() - - def get_history(self, username, block_key, scope=Scope.user_state): - """We don't guarantee that history for many blocks will be fast.""" - raise NotImplementedError() - - def iter_all_for_block(self, block_key, scope=Scope.user_state, batch_size=None): - """ - You get no ordering guarantees. Fetching will happen in batch_size - increments. If you're using this method, you should be running in an - async task. - """ - raise NotImplementedError() - - def iter_all_for_course(self, course_key, block_type=None, scope=Scope.user_state, batch_size=None): - """ - You get no ordering guarantees. Fetching will happen in batch_size - increments. If you're using this method, you should be running in an - async task. - """ - raise NotImplementedError() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 776ee30627..e932dbf34a 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -179,7 +179,9 @@ YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) -if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or FEATURES.get('ENABLE_DASHBOARD_SEARCH'): +if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \ + FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \ + FEATURES.get('ENABLE_COURSE_DISCOVERY'): # Use MockSearchEngine as the search engine for test scenario SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 23163c3c96..5a497f425f 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -16,6 +16,7 @@ Common traits: # and throws spurious errors. Therefore, we disable invalid-name checking. # pylint: disable=invalid-name +import datetime import json from .common import * @@ -107,6 +108,7 @@ CELERY_QUEUES = { if os.environ.get('QUEUE') == 'high_mem': CELERYD_MAX_TASKS_PER_CHILD = 1 +CELERYBEAT_SCHEDULE = {} # For scheduling tasks, entries can be added to this dict ########################## NON-SECURE ENV CONFIG ############################## # Things like server locations, ports, etc. @@ -155,6 +157,12 @@ SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True) REGISTRATION_EXTRA_FIELDS = ENV_TOKENS.get('REGISTRATION_EXTRA_FIELDS', REGISTRATION_EXTRA_FIELDS) SESSION_COOKIE_SECURE = ENV_TOKENS.get('SESSION_COOKIE_SECURE', SESSION_COOKIE_SECURE) +# Set the names of cookies shared with the marketing site +# These have the same cookie domain as the session, which in production +# usually includes subdomains. +EDXMKTG_LOGGED_IN_COOKIE_NAME = ENV_TOKENS.get('EDXMKTG_LOGGED_IN_COOKIE_NAME', EDXMKTG_LOGGED_IN_COOKIE_NAME) +EDXMKTG_USER_INFO_COOKIE_NAME = ENV_TOKENS.get('EDXMKTG_USER_INFO_COOKIE_NAME', EDXMKTG_USER_INFO_COOKIE_NAME) + CMS_BASE = ENV_TOKENS.get('CMS_BASE', 'studio.edx.org') # allow for environments to specify what cookie name our login subsystem should use @@ -530,10 +538,27 @@ TIME_ZONE_DISPLAYED_FOR_DEADLINES = ENV_TOKENS.get("TIME_ZONE_DISPLAYED_FOR_DEAD X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS) ##### Third-party auth options ################################################ -THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH) +if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): + AUTHENTICATION_BACKENDS = ( + ENV_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [ + 'social.backends.google.GoogleOAuth2', + 'social.backends.linkedin.LinkedinOAuth2', + 'social.backends.facebook.FacebookOAuth2', + 'third_party_auth.saml.SAMLAuthBackend', + ]) + list(AUTHENTICATION_BACKENDS) + ) -# The reduced session expiry time during the third party login pipeline. (Value in seconds) -SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600) + # The reduced session expiry time during the third party login pipeline. (Value in seconds) + SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600) + + # third_party_auth config moved to ConfigurationModels. This is for data migration only: + THIRD_PARTY_AUTH_OLD_CONFIG = AUTH_TOKENS.get('THIRD_PARTY_AUTH', None) + + if ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24) is not None: + CELERYBEAT_SCHEDULE['refresh-saml-metadata'] = { + 'task': 'third_party_auth.fetch_saml_metadata', + 'schedule': datetime.timedelta(hours=ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24)), + } ##### OAUTH2 Provider ############## if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): @@ -587,7 +612,9 @@ PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = ENV_TOKENS.get( 'PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM', PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM ) -if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or FEATURES.get('ENABLE_DASHBOARD_SEARCH'): +if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \ + FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \ + FEATURES.get('ENABLE_COURSE_DISCOVERY'): # Use ElasticSearch as the search engine herein SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" @@ -641,6 +668,10 @@ EDXNOTES_INTERNAL_API = ENV_TOKENS.get('EDXNOTES_INTERNAL_API', EDXNOTES_INTERNA CREDIT_PROVIDER_SECRET_KEYS = AUTH_TOKENS.get("CREDIT_PROVIDER_SECRET_KEYS", {}) - ############ CERTIFICATE VERIFICATION URL (STATIC FILES) ########### ENV_TOKENS.get('CERTIFICATES_STATIC_VERIFY_URL', CERTIFICATES_STATIC_VERIFY_URL) + +##################### LTI Provider ##################### +if FEATURES.get('ENABLE_LTI_PROVIDER'): + INSTALLED_APPS += ('lti_provider',) + AUTHENTICATION_BACKENDS += ('lti_provider.users.LtiBackend', ) diff --git a/lms/envs/bok_choy.auth.json b/lms/envs/bok_choy.auth.json index 85a19928cc..719f72ccf9 100644 --- a/lms/envs/bok_choy.auth.json +++ b/lms/envs/bok_choy.auth.json @@ -117,17 +117,6 @@ "username": "lms" }, "SECRET_KEY": "", - "THIRD_PARTY_AUTH": { - "Dummy": {}, - "Google": { - "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test", - "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test" - }, - "Facebook": { - "SOCIAL_AUTH_FACEBOOK_KEY": "test", - "SOCIAL_AUTH_FACEBOOK_SECRET": "test" - } - }, "DJFS": { "type": "s3fs", "bucket": "test", diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json index 8fde6044b5..5be8f73c84 100644 --- a/lms/envs/bok_choy.env.json +++ b/lms/envs/bok_choy.env.json @@ -79,7 +79,6 @@ "ENABLE_INSTRUCTOR_ANALYTICS": true, "ENABLE_S3_GRADE_DOWNLOADS": true, "ENABLE_THIRD_PARTY_AUTH": true, - "ENABLE_DUMMY_THIRD_PARTY_AUTH_PROVIDER": true, "ENABLE_COMBINED_LOGIN_REGISTRATION": true, "PREVIEW_LMS_BASE": "localhost:8003", "SUBDOMAIN_BRANDING": false, @@ -119,6 +118,13 @@ "SYSLOG_SERVER": "", "TECH_SUPPORT_EMAIL": "technical@example.com", "THEME_NAME": "", + "THIRD_PARTY_AUTH_BACKENDS": [ + "social.backends.google.GoogleOAuth2", + "social.backends.linkedin.LinkedinOAuth2", + "social.backends.facebook.FacebookOAuth2", + "third_party_auth.dummy.DummyBackend", + "third_party_auth.saml.SAMLAuthBackend" + ], "TIME_ZONE": "America/New_York", "WIKI_ENABLED": true } diff --git a/lms/envs/common.py b/lms/envs/common.py index 297f9913ce..03193858c5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -535,11 +535,6 @@ AUTHENTICATION_BACKENDS = ( STUDENT_FILEUPLOAD_MAX_SIZE = 4 * 1000 * 1000 # 4 MB MAX_FILEUPLOADS_PER_INPUT = 20 -# FIXME: -# We should have separate S3 staged URLs in case we need to make changes to -# these assets and test them. -LIB_URL = '/static/js/' - # Dev machines shouldn't need the book # BOOK_URL = '/static/book/' BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' # For AWS deploys @@ -1147,17 +1142,23 @@ MIDDLEWARE_CLASSES = ( 'splash.middleware.SplashMiddleware', - # Allows us to dark-launch particular languages - 'dark_lang.middleware.DarkLangMiddleware', + 'geoinfo.middleware.CountryMiddleware', 'embargo.middleware.EmbargoMiddleware', # 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', + # Allows us to dark-launch particular languages. + # Must be after LangPrefMiddleware, so ?preview-lang query params can override + # user's language preference. ?clear-lang resets to user's language preference. + 'dark_lang.middleware.DarkLangMiddleware', + + # Detects user-requested locale from 'accept-language' header in http request. + # Must be after DarkLangMiddleware. + # TODO: Re-import the Django version once we upgrade to Django 1.8 [PLAT-671] + # 'django.middleware.locale.LocaleMiddleware', + 'django_locale.middleware.LocaleMiddleware', 'django.middleware.transaction.TransactionMiddleware', # 'debug_toolbar.middleware.DebugToolbarMiddleware', @@ -1270,9 +1271,11 @@ student_account_js = [ 'js/student_account/models/PasswordResetModel.js', 'js/student_account/views/FormView.js', 'js/student_account/views/LoginView.js', + 'js/student_account/views/HintedLoginView.js', 'js/student_account/views/RegisterView.js', 'js/student_account/views/PasswordResetView.js', 'js/student_account/views/AccessView.js', + 'js/student_account/views/InstitutionLoginView.js', 'js/student_account/accessApp.js', ] @@ -1305,6 +1308,20 @@ verify_student_js = [ ] reverify_js = [ + 'js/verify_student/views/error_view.js', + 'js/verify_student/views/image_input_view.js', + 'js/verify_student/views/webcam_photo_view.js', + 'js/verify_student/views/step_view.js', + 'js/verify_student/views/face_photo_step_view.js', + 'js/verify_student/views/id_photo_step_view.js', + 'js/verify_student/views/review_photos_step_view.js', + 'js/verify_student/views/reverify_success_step_view.js', + 'js/verify_student/models/verification_model.js', + 'js/verify_student/views/reverify_view.js', + 'js/verify_student/reverify.js', +] + +incourse_reverify_js = [ 'js/verify_student/views/error_view.js', 'js/verify_student/views/image_input_view.js', 'js/verify_student/views/webcam_photo_view.js', @@ -1323,6 +1340,12 @@ certificates_web_view_js = [ 'js/src/logger.js', ] +credit_web_view_js = [ + 'js/vendor/jquery.min.js', + 'js/vendor/jquery.cookie.js', + 'js/src/logger.js', +] + PIPELINE_CSS = { 'style-vendor': { 'source_filenames': [ @@ -1539,6 +1562,10 @@ PIPELINE_JS = { 'source_filenames': reverify_js, 'output_filename': 'js/reverify.js' }, + 'incourse_reverify': { + 'source_filenames': incourse_reverify_js, + 'output_filename': 'js/incourse_reverify.js' + }, 'ccx': { 'source_filenames': ccx_js, 'output_filename': 'js/ccx.js' @@ -1558,6 +1585,10 @@ PIPELINE_JS = { 'utility': { 'source_filenames': ['js/src/utility.js'], 'output_filename': 'js/utility.js' + }, + 'credit_wv': { + 'source_filenames': credit_web_view_js, + 'output_filename': 'js/credit/web_view.js' } } @@ -1869,6 +1900,7 @@ INSTALLED_APPS = ( 'lms.djangoapps.lms_xblock', + 'openedx.core.djangoapps.content.course_overviews', 'openedx.core.djangoapps.content.course_structures', 'course_structure_api', @@ -1895,7 +1927,10 @@ CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52 ######################### MARKETING SITE ############################### -EDXMKTG_COOKIE_NAME = 'edxloggedin' +EDXMKTG_LOGGED_IN_COOKIE_NAME = 'edxloggedin' +EDXMKTG_USER_INFO_COOKIE_NAME = 'edx-user-info' +EDXMKTG_USER_INFO_COOKIE_VERSION = 1 + MKTG_URLS = {} MKTG_URL_LINK_MAP = { 'ABOUT': 'about', @@ -2019,6 +2054,10 @@ FEATURES['CLASS_DASHBOARD'] = False if FEATURES.get('CLASS_DASHBOARD'): INSTALLED_APPS += ('class_dashboard',) +################ Enable credit eligibility feature #################### +ENABLE_CREDIT_ELIGIBILITY = False +FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY + ######################## CAS authentication ########################### if FEATURES.get('AUTH_USE_CAS'): @@ -2348,10 +2387,6 @@ for app_name in OPTIONAL_APPS: continue INSTALLED_APPS += (app_name,) -# Stub for third_party_auth options. -# See common/djangoapps/third_party_auth/settings.py for configuration details. -THIRD_PARTY_AUTH = {} - ### ADVANCED_SECURITY_CONFIG # Empty by default ADVANCED_SECURITY_CONFIG = {} @@ -2512,3 +2547,9 @@ CREDIT_PROVIDER_SECRET_KEYS = {} # when a credit provider notifies us that a student has been approved # or denied for credit. CREDIT_PROVIDER_TIMESTAMP_EXPIRATION = 15 * 60 + +# Default domain for the e-mail address associated with users who are created +# via the LTI Provider feature. Note that the generated e-mail addresses are +# not expected to be active; this setting simply allows administrators to +# route any messages intended for LTI users to a common domain. +LTI_USER_EMAIL_DOMAIN = 'lti.example.com' diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 4960594a87..52f6a83a40 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -121,7 +121,7 @@ FEATURES['LICENSING'] = True ########################## Courseware Search ####################### -FEATURES['ENABLE_COURSEWARE_SEARCH'] = True +FEATURES['ENABLE_COURSEWARE_SEARCH'] = False SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" @@ -170,6 +170,10 @@ FEATURES['STORE_BILLING_INFO'] = True FEATURES['ENABLE_PAID_COURSE_REGISTRATION'] = True FEATURES['ENABLE_COSMETIC_DISPLAY_PRICE'] = True +########################## Third Party Auth ####################### + +if FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and 'third_party_auth.dummy.DummyBackend' not in AUTHENTICATION_BACKENDS: + AUTHENTICATION_BACKENDS = ['third_party_auth.dummy.DummyBackend'] + list(AUTHENTICATION_BACKENDS) ##################################################################### # See if the developer has any local overrides. diff --git a/lms/envs/test.py b/lms/envs/test.py index 9803879579..d84a0d2fd3 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -238,18 +238,13 @@ PASSWORD_COMPLEXITY = {} ######### Third-party auth ########## FEATURES['ENABLE_THIRD_PARTY_AUTH'] = True -THIRD_PARTY_AUTH = { - "Google": { - "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY": "test", - "SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET": "test", - }, - "Facebook": { - "SOCIAL_AUTH_FACEBOOK_KEY": "test", - "SOCIAL_AUTH_FACEBOOK_SECRET": "test", - }, -} - -FEATURES['ENABLE_DUMMY_THIRD_PARTY_AUTH_PROVIDER'] = True +AUTHENTICATION_BACKENDS = ( + 'social.backends.google.GoogleOAuth2', + 'social.backends.linkedin.LinkedinOAuth2', + 'social.backends.facebook.FacebookOAuth2', + 'third_party_auth.dummy.DummyBackend', + 'third_party_auth.saml.SAMLAuthBackend', +) + AUTHENTICATION_BACKENDS ################################## OPENID ##################################### FEATURES['AUTH_USE_OPENID'] = True @@ -459,7 +454,7 @@ FEATURES['ENABLE_EDXNOTES'] = True FEATURES['ENABLE_TEAMS'] = True # Add milestones to Installed apps for testing -INSTALLED_APPS += ('milestones', ) +INSTALLED_APPS += ('milestones', 'openedx.core.djangoapps.call_stack_manager') # Enable courseware search for tests FEATURES['ENABLE_COURSEWARE_SEARCH'] = True @@ -495,3 +490,4 @@ PROFILE_IMAGE_MIN_BYTES = 100 # Enable the LTI provider feature for testing FEATURES['ENABLE_LTI_PROVIDER'] = True INSTALLED_APPS += ('lti_provider',) +AUTHENTICATION_BACKENDS += ('lti_provider.users.LtiBackend',) diff --git a/lms/envs/test_static_optimized.py b/lms/envs/test_static_optimized.py new file mode 100644 index 0000000000..61c99b6590 --- /dev/null +++ b/lms/envs/test_static_optimized.py @@ -0,0 +1,16 @@ +""" +Settings used when generating static assets for use in tests. + +Bok Choy uses two different settings files: +1. test_static_optimized is used when invoking collectstatic +2. bok_choy is used when running CMS and LMS + +Note: it isn't possible to have a single settings file, because Django doesn't +support both generating static assets to a directory and also serving static +from the same directory. + +""" + +# TODO: update the Bok Choy tests to run with optimized static assets (as is done in Studio) + +from .bok_choy import * # pylint: disable=wildcard-import, unused-wildcard-import diff --git a/lms/startup.py b/lms/startup.py index 3add027f15..4ca979ce45 100644 --- a/lms/startup.py +++ b/lms/startup.py @@ -141,4 +141,4 @@ def enable_third_party_auth(): """ from third_party_auth import settings as auth_settings - auth_settings.apply_settings(settings.THIRD_PARTY_AUTH, settings) + auth_settings.apply_settings(settings) diff --git a/lms/static/js/components/tabbed/views/tabbed_view.js b/lms/static/js/components/tabbed/views/tabbed_view.js index a3c4dfd8c5..77bc1b4210 100644 --- a/lms/static/js/components/tabbed/views/tabbed_view.js +++ b/lms/static/js/components/tabbed/views/tabbed_view.js @@ -41,12 +41,14 @@ }, setActiveTab: function (index) { + var tab = this.tabs[index], + view = tab.view; this.$('a.is-active').removeClass('is-active').attr('aria-selected', 'false'); this.$('a[data-index='+index+']').addClass('is-active').attr('aria-selected', 'true'); - var view = this.tabs[index].view; view.render(); this.$('.page-content-main').html(view.$el.html()); this.$('.sr-is-focusable').focus(); + this.router.navigate(tab.url, {replace: true}); }, switchTab: function (event) { diff --git a/lms/static/js/courseware/credit_progress.js b/lms/static/js/courseware/credit_progress.js index d0600b85c0..61c941206e 100644 --- a/lms/static/js/courseware/credit_progress.js +++ b/lms/static/js/courseware/credit_progress.js @@ -1,11 +1,17 @@ $(document).ready(function() { + var container = $('.requirement-container'); + var collapse = container.data('eligible'); + if (collapse == 'not_eligible') { + container.addClass('is-hidden'); + $('.detail-collapse').find('.fa').toggleClass('fa-caret-up fa-caret-down'); + $('.requirement-detail').text(gettext('More')); + } $('.detail-collapse').on('click', function() { var el = $(this); - $('.requirement-container').toggleClass('is-hidden'); - el.find('.fa').toggleClass('fa-caret-down fa-caret-up'); + container.toggleClass('is-hidden'); + el.find('.fa').toggleClass('fa-caret-up fa-caret-down'); el.find('.requirement-detail').text(function(i, text){ - return text === gettext('More') ? gettext('Less') : gettext('More'); + return text === gettext('Less') ? gettext('More') : gettext('Less'); }); }); - }); diff --git a/lms/static/js/help.js b/lms/static/js/help.js deleted file mode 100644 index dc6e22b1fb..0000000000 --- a/lms/static/js/help.js +++ /dev/null @@ -1,10 +0,0 @@ -$(document).ready(function() { - var open_question = ""; - var question_id; - - $('.response').click(function(){ - $(this).toggleClass('opened'); - answer = $(this).find(".answer"); - answer.slideToggle('fast'); - }); -}); diff --git a/lms/static/js/html5shiv.js b/lms/static/js/html5shiv.js deleted file mode 100644 index 1ec510f2a4..0000000000 --- a/lms/static/js/html5shiv.js +++ /dev/null @@ -1,3 +0,0 @@ -// HTML5 Shiv v3 | @jon_neal @afarkas @rem | MIT/GPL2 Licensed -// Uncompressed source: https://github.com/aFarkas/html5shiv -(function(a,b){function f(a){var c,d,e,f;b.documentMode>7?(c=b.createElement("font"),c.setAttribute("data-html5shiv",a.nodeName.toLowerCase())):c=b.createElement("shiv:"+a.nodeName);while(a.firstChild)c.appendChild(a.childNodes[0]);for(d=a.attributes,e=d.length,f=0;f7?e[g][e[g].length-1]=e[g][e[g].length-1].replace(d,'$1font[data-html5shiv="$2"]'):e[g][e[g].length-1]=e[g][e[g].length-1].replace(d,"$1shiv\\:$2"),e[g]=e[g].join("}");return e.join("{")}var c=function(a){return a.innerHTML="",a.childNodes.length===1}(b.createElement("a")),d=function(a,b,c){return b.appendChild(a),(c=(c?c(a):a.currentStyle).display)&&b.removeChild(a)&&c==="block"}(b.createElement("nav"),b.documentElement,a.getComputedStyle),e={elements:"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),shivDocument:function(a){a=a||b;if(a.documentShived)return;a.documentShived=!0;var f=a.createElement,g=a.createDocumentFragment,h=a.getElementsByTagName("head")[0],i=function(a){f(a)};c||(e.elements.join(" ").replace(/\w+/g,i),a.createElement=function(a){var b=f(a);return b.canHaveChildren&&e.shivDocument(b.document),b},a.createDocumentFragment=function(){return e.shivDocument(g())});if(!d&&h){var j=f("div");j.innerHTML=["x"].join(""),h.insertBefore(j.lastChild,h.firstChild)}return a}};e.shivDocument(b),a.html5=e;if(c||!a.attachEvent)return;a.attachEvent("onbeforeprint",function(){if(a.html5.supportsXElement||!b.namespaces)return;b.namespaces.shiv||b.namespaces.add("shiv");var c=-1,d=new RegExp("^("+a.html5.elements.join("|")+")$","i"),e=b.getElementsByTagName("*"),g=e.length,j,k=i(h(function(a,b){var c=[],d=a.length;while(d)c.unshift(a[--d]);d=b.length;while(d)c.unshift(b[--d]);c.sort(function(a,b){return a.sourceIndex-b.sourceIndex}),d=c.length;while(d)c[--d]=c[d].styleSheet;return c}(b.getElementsByTagName("style"),b.getElementsByTagName("link"))));while(++cthis.nInputChars && this.aSuggestions.length && this.oP.cache) - { - var arr = []; - for (var i=0;i" + val.substring(st, st+this.sInput.length) + "" + val.substring(st+this.sInput.length); - - - var span = _bsn.DOM.createElement("span", {}, output, true); - if (arr[i].info != "") - { - var br = _bsn.DOM.createElement("br", {}); - span.appendChild(br); - var small = _bsn.DOM.createElement("small", {}, arr[i].info); - span.appendChild(small); - } - - var a = _bsn.DOM.createElement("a", { href:"#" }); - - var tl = _bsn.DOM.createElement("span", {className:"tl"}, " "); - var tr = _bsn.DOM.createElement("span", {className:"tr"}, " "); - a.appendChild(tl); - a.appendChild(tr); - - a.appendChild(span); - - a.name = i+1; - a.onclick = function () { pointer.setHighlightedValue(); return false; } - a.onmouseover = function () { pointer.setHighlight(this.name); } - - var li = _bsn.DOM.createElement( "li", {}, a ); - - ul.appendChild( li ); - } - - - // no results - // - if (arr.length == 0) - { - var li = _bsn.DOM.createElement( "li", {className:"as_warning"}, this.oP.noresults ); - - ul.appendChild( li ); - } - - - div.appendChild( ul ); - - - var fcorner = _bsn.DOM.createElement("div", {className:"as_corner"}); - var fbar = _bsn.DOM.createElement("div", {className:"as_bar"}); - var footer = _bsn.DOM.createElement("div", {className:"as_footer"}); - footer.appendChild(fcorner); - footer.appendChild(fbar); - div.appendChild(footer); - - - - // get position of target textfield - // position holding div below it - // set width of holding div to width of field - // - var pos = _bsn.DOM.getPos(this.fld); - - div.style.left = pos.x + "px"; - div.style.top = ( pos.y + this.fld.offsetHeight + this.oP.offsety ) + "px"; - div.style.width = this.fld.offsetWidth + "px"; - - - - // set mouseover functions for div - // when mouse pointer leaves div, set a timeout to remove the list after an interval - // when mouse enters div, kill the timeout so the list won't be removed - // - div.onmouseover = function(){ pointer.killTimeout() } - div.onmouseout = function(){ pointer.resetTimeout() } - - - // add DIV to document - // - document.getElementsByTagName("body")[0].appendChild(div); - - - - // currently no item is highlighted - // - this.iHighlighted = 0; - - - - - - - // remove list after an interval - // - var pointer = this; - this.toID = setTimeout(function () { pointer.clearSuggestions() }, this.oP.timeout); -} - - - - - - - - - - - - - - - -_bsn.AutoSuggest.prototype.changeHighlight = function(key) -{ - var list = _bsn.DOM.getElement("as_ul"); - if (!list) - return false; - - var n; - - if (key == 40) - n = this.iHighlighted + 1; - else if (key == 38) - n = this.iHighlighted - 1; - - - if (n > list.childNodes.length) - n = list.childNodes.length; - if (n < 1) - n = 1; - - - this.setHighlight(n); -} - - - -_bsn.AutoSuggest.prototype.setHighlight = function(n) -{ - var list = _bsn.DOM.getElement("as_ul"); - if (!list) - return false; - - if (this.iHighlighted > 0) - this.clearHighlight(); - - this.iHighlighted = Number(n); - - list.childNodes[this.iHighlighted-1].className = "as_highlight"; - - - this.killTimeout(); -} - - -_bsn.AutoSuggest.prototype.clearHighlight = function() -{ - var list = _bsn.DOM.getElement("as_ul"); - if (!list) - return false; - - if (this.iHighlighted > 0) - { - list.childNodes[this.iHighlighted-1].className = ""; - this.iHighlighted = 0; - } -} - - -_bsn.AutoSuggest.prototype.setHighlightedValue = function () -{ - if (this.iHighlighted) - { - this.sInput = this.fld.value = this.aSuggestions[ this.iHighlighted-1 ].value; - - // move cursor to end of input (safari) - // - this.fld.focus(); - if (this.fld.selectionStart) - this.fld.setSelectionRange(this.sInput.length, this.sInput.length); - - - this.clearSuggestions(); - - // pass selected object to callback function, if exists - // - if (typeof(this.oP.callback) == "function") - this.oP.callback( this.aSuggestions[this.iHighlighted-1] ); - } -} - - - - - - - - - - - - - -_bsn.AutoSuggest.prototype.killTimeout = function() -{ - clearTimeout(this.toID); -} - -_bsn.AutoSuggest.prototype.resetTimeout = function() -{ - clearTimeout(this.toID); - var pointer = this; - this.toID = setTimeout(function () { pointer.clearSuggestions() }, 1000); -} - - - - - - - -_bsn.AutoSuggest.prototype.clearSuggestions = function () -{ - - this.killTimeout(); - - var ele = _bsn.DOM.getElement(this.idAs); - var pointer = this; - if (ele) - { - var fade = new _bsn.Fader(ele,1,0,250,function () { _bsn.DOM.removeElement(pointer.idAs) }); - } -} - - - - - - - - - - -// AJAX PROTOTYPE _____________________________________________ - - -if (typeof(_bsn.Ajax) == "undefined") - _bsn.Ajax = {} - - - -_bsn.Ajax = function () -{ - this.req = {}; - this.isIE = false; -} - - - -_bsn.Ajax.prototype.makeRequest = function (url, meth, onComp, onErr) -{ - - if (meth != "POST") - meth = "GET"; - - this.onComplete = onComp; - this.onError = onErr; - - var pointer = this; - - // branch for native XMLHttpRequest object - if (window.XMLHttpRequest) - { - this.req = new XMLHttpRequest(); - this.req.onreadystatechange = function () { pointer.processReqChange() }; - this.req.open("GET", url, true); // - this.req.send(null); - // branch for IE/Windows ActiveX version - } - else if (window.ActiveXObject) - { - this.req = new ActiveXObject("Microsoft.XMLHTTP"); - if (this.req) - { - this.req.onreadystatechange = function () { pointer.processReqChange() }; - this.req.open(meth, url, true); - this.req.send(); - } - } -} - - -_bsn.Ajax.prototype.processReqChange = function() -{ - - // only if req shows "loaded" - if (this.req.readyState == 4) { - // only if "OK" - if (this.req.status == 200) - { - this.onComplete( this.req ); - } else { - this.onError( this.req.status ); - } - } -} - - - - - - - - - - -// DOM PROTOTYPE _____________________________________________ - - -if (typeof(_bsn.DOM) == "undefined") - _bsn.DOM = {} - - - - -_bsn.DOM.createElement = function ( type, attr, cont, html ) -{ - var ne = document.createElement( type ); - if (!ne) - return false; - - for (var a in attr) - ne[a] = attr[a]; - - if (typeof(cont) == "string" && !html) - ne.appendChild( document.createTextNode(cont) ); - else if (typeof(cont) == "string" && html) - ne.innerHTML = cont; - else if (typeof(cont) == "object") - ne.appendChild( cont ); - - return ne; -} - - - - - -_bsn.DOM.clearElement = function ( id ) -{ - var ele = this.getElement( id ); - - if (!ele) - return false; - - while (ele.childNodes.length) - ele.removeChild( ele.childNodes[0] ); - - return true; -} - - - - - - - - - -_bsn.DOM.removeElement = function ( ele ) -{ - var e = this.getElement(ele); - - if (!e) - return false; - else if (e.parentNode.removeChild(e)) - return true; - else - return false; -} - - - - - -_bsn.DOM.replaceContent = function ( id, cont, html ) -{ - var ele = this.getElement( id ); - - if (!ele) - return false; - - this.clearElement( ele ); - - if (typeof(cont) == "string" && !html) - ele.appendChild( document.createTextNode(cont) ); - else if (typeof(cont) == "string" && html) - ele.innerHTML = cont; - else if (typeof(cont) == "object") - ele.appendChild( cont ); -} - - - - - - - - - -_bsn.DOM.getElement = function ( ele ) -{ - if (typeof(ele) == "undefined") - { - return false; - } - else if (typeof(ele) == "string") - { - var re = document.getElementById( ele ); - if (!re) - return false; - else if (typeof(re.appendChild) != "undefined" ) { - return re; - } else { - return false; - } - } - else if (typeof(ele.appendChild) != "undefined") - return ele; - else - return false; -} - - - - - - - -_bsn.DOM.appendChildren = function ( id, arr ) -{ - var ele = this.getElement( id ); - - if (!ele) - return false; - - - if (typeof(arr) != "object") - return false; - - for (var i=0;i
      '); - }); - - it('retake photo', function () { - spyOn(window, "refereshPageMessage").andCallFake(function () { - return; - }); - spyOn($, "ajax").andCallFake(function (e) { - e.success({"success": false}); - }); - submitToPaymentProcessing(); - expect(window.refereshPageMessage).toHaveBeenCalled(); - }); - - it('successful submission', function () { - spyOn(window, "submitForm").andCallFake(function () { - return; - }); - spyOn($, "ajax").andCallFake(function (e) { - e.success({"success": true}); - }); - submitToPaymentProcessing(); - expect(window.submitForm).toHaveBeenCalled(); - expect($(".payment-button")).toHaveClass("is-disabled"); - }); - - it('Error during process', function () { - spyOn(window, "showSubmissionError").andCallFake(function () { - return; - }); - spyOn($, "ajax").andCallFake(function (e) { - e.error({}); - }); - spyOn($.fn, "addClass").andCallThrough(); - spyOn($.fn, "removeClass").andCallThrough(); - - submitToPaymentProcessing(); - expect(window.showSubmissionError).toHaveBeenCalled(); - - // make sure the button isn't disabled - expect($(".payment-button")).not.toHaveClass("is-disabled"); - - // but also make sure that it was disabled during the ajax call - expect($.fn.addClass).toHaveBeenCalledWith("is-disabled"); - expect($.fn.removeClass).toHaveBeenCalledWith("is-disabled"); - }); - - }); - }); diff --git a/lms/static/js/spec/student_account/access_spec.js b/lms/static/js/spec/student_account/access_spec.js index a82da514e2..590c9df58f 100644 --- a/lms/static/js/spec/student_account/access_spec.js +++ b/lms/static/js/spec/student_account/access_spec.js @@ -58,6 +58,7 @@ define([ thirdPartyAuth: { currentProvider: null, providers: [], + secondaryProviders: [{name: "provider"}], finishAuthUrl: finishAuthUrl }, nextUrl: nextUrl, // undefined for default @@ -97,6 +98,8 @@ define([ TemplateHelpers.installTemplate('templates/student_account/register'); TemplateHelpers.installTemplate('templates/student_account/password_reset'); TemplateHelpers.installTemplate('templates/student_account/form_field'); + TemplateHelpers.installTemplate('templates/student_account/institution_login'); + TemplateHelpers.installTemplate('templates/student_account/institution_register'); // Stub analytics tracking window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'pageview', 'trackLink']); @@ -135,6 +138,30 @@ define([ assertForms('#login-form', '#register-form'); }); + it('toggles between the login and institution login view', function() { + ajaxSpyAndInitialize(this, 'login'); + + // Simulate clicking on institution login button + $('#login-form .button-secondary-login[data-type="institution_login"]').click(); + assertForms('#institution_login-form', '#login-form'); + + // Simulate selection of the login form + selectForm('login'); + assertForms('#login-form', '#institution_login-form'); + }); + + it('toggles between the register and institution register view', function() { + ajaxSpyAndInitialize(this, 'register'); + + // Simulate clicking on institution login button + $('#register-form .button-secondary-login[data-type="institution_login"]').click(); + assertForms('#institution_login-form', '#register-form'); + + // Simulate selection of the login form + selectForm('register'); + assertForms('#register-form', '#institution_login-form'); + }); + it('displays the reset password form', function() { ajaxSpyAndInitialize(this, 'login'); diff --git a/lms/static/js/spec/student_account/account_settings_factory_spec.js b/lms/static/js/spec/student_account/account_settings_factory_spec.js index df1aaa658a..fc7483eaa5 100644 --- a/lms/static/js/spec/student_account/account_settings_factory_spec.js +++ b/lms/static/js/spec/student_account/account_settings_factory_spec.js @@ -32,12 +32,14 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers var AUTH_DATA = { 'providers': [ { + 'id': 'oa2-network1', 'name': "Network1", 'connected': true, 'connect_url': 'yetanother1.com/auth/connect', 'disconnect_url': 'yetanother1.com/auth/disconnect' }, { + 'id': 'oa2-network2', 'name': "Network2", 'connected': true, 'connect_url': 'yetanother2.com/auth/connect', diff --git a/lms/static/js/spec/student_account/hinted_login_spec.js b/lms/static/js/spec/student_account/hinted_login_spec.js new file mode 100644 index 0000000000..b6f346a56e --- /dev/null +++ b/lms/static/js/spec/student_account/hinted_login_spec.js @@ -0,0 +1,71 @@ +define([ + 'jquery', + 'underscore', + 'common/js/spec_helpers/template_helpers', + 'common/js/spec_helpers/ajax_helpers', + 'js/student_account/views/HintedLoginView', +], function($, _, TemplateHelpers, AjaxHelpers, HintedLoginView) { + 'use strict'; + describe('edx.student.account.HintedLoginView', function() { + + var view = null, + requests = null, + PLATFORM_NAME = 'edX', + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + iconClass: 'fa-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + id: 'oa2-facebook', + name: 'Facebook', + iconClass: 'fa-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ] + }, + HINTED_PROVIDER = "oa2-google-oauth2"; + + var createHintedLoginView = function(test) { + // Initialize the login view + view = new HintedLoginView({ + thirdPartyAuth: THIRD_PARTY_AUTH, + hintedProvider: HINTED_PROVIDER, + platformName: PLATFORM_NAME + }); + + // Mock the redirect call + spyOn( view, 'redirect' ).andCallFake( function() {} ); + + view.render(); + }; + + beforeEach(function() { + setFixtures('
      '); + TemplateHelpers.installTemplate('templates/student_account/hinted_login'); + }); + + it('displays a choice as two buttons', function() { + createHintedLoginView(this); + + expect($('.proceed-button.button-oa2-google-oauth2')).toBeVisible(); + expect($('.form-toggle')).toBeVisible(); + expect($('.proceed-button.button-oa2-facebook')).not.toBeVisible(); + }); + + it('redirects the user to the hinted provider if the user clicks the proceed button', function() { + createHintedLoginView(this); + + // Click the "Yes, proceed" button + $('.proceed-button').click(); + + expect(view.redirect).toHaveBeenCalledWith( '/auth/login/google-oauth2/?auth_entry=account_login' ); + }); + }); +}); diff --git a/lms/static/js/spec/student_account/institution_login_spec.js b/lms/static/js/spec/student_account/institution_login_spec.js new file mode 100644 index 0000000000..208c975550 --- /dev/null +++ b/lms/static/js/spec/student_account/institution_login_spec.js @@ -0,0 +1,80 @@ +define([ + 'jquery', + 'underscore', + 'common/js/spec_helpers/template_helpers', + 'js/student_account/views/InstitutionLoginView', +], function($, _, TemplateHelpers, InstitutionLoginView) { + 'use strict'; + describe('edx.student.account.InstitutionLoginView', function() { + + var view = null, + PLATFORM_NAME = 'edX', + THIRD_PARTY_AUTH = { + currentProvider: null, + providers: [], + secondaryProviders: [ + { + id: 'oa2-google-oauth2', + name: 'Google', + iconClass: 'fa-google-plus', + loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', + registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' + }, + { + id: 'oa2-facebook', + name: 'Facebook', + iconClass: 'fa-facebook', + loginUrl: '/auth/login/facebook/?auth_entry=account_login', + registerUrl: '/auth/login/facebook/?auth_entry=account_register' + } + ] + }; + + var createInstLoginView = function(mode) { + // Initialize the login view + view = new InstitutionLoginView({ + mode: mode, + thirdPartyAuth: THIRD_PARTY_AUTH, + platformName: PLATFORM_NAME + }); + view.render(); + }; + + beforeEach(function() { + setFixtures('
      '); + TemplateHelpers.installTemplate('templates/student_account/institution_login'); + TemplateHelpers.installTemplate('templates/student_account/institution_register'); + }); + + it('displays a list of providers', function() { + createInstLoginView('login'); + expect($('#institution_login-form').html()).not.toBe(""); + var $google = $('li a:contains("Google")'); + expect($google).toBeVisible(); + expect($google).toHaveAttr( + 'href', '/auth/login/google-oauth2/?auth_entry=account_login' + ); + var $facebook = $('li a:contains("Facebook")'); + expect($facebook).toBeVisible(); + expect($facebook).toHaveAttr( + 'href', '/auth/login/facebook/?auth_entry=account_login' + ); + }); + + it('displays a list of providers', function() { + createInstLoginView('register'); + expect($('#institution_login-form').html()).not.toBe(""); + var $google = $('li a:contains("Google")'); + expect($google).toBeVisible(); + expect($google).toHaveAttr( + 'href', '/auth/login/google-oauth2/?auth_entry=account_register' + ); + var $facebook = $('li a:contains("Facebook")'); + expect($facebook).toBeVisible(); + expect($facebook).toHaveAttr( + 'href', '/auth/login/facebook/?auth_entry=account_register' + ); + }); + + }); +}); diff --git a/lms/static/js/spec/student_account/login_spec.js b/lms/static/js/spec/student_account/login_spec.js index 30ff0f10d3..d61f49be87 100644 --- a/lms/static/js/spec/student_account/login_spec.js +++ b/lms/static/js/spec/student_account/login_spec.js @@ -25,12 +25,14 @@ define([ currentProvider: null, providers: [ { + id: 'oa2-google-oauth2', name: 'Google', iconClass: 'fa-google-plus', loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' }, { + id: 'oa2-facebook', name: 'Facebook', iconClass: 'fa-facebook', loginUrl: '/auth/login/facebook/?auth_entry=account_login', @@ -195,8 +197,8 @@ define([ createLoginView(this); // Verify that Google and Facebook registration buttons are displayed - expect($('.button-Google')).toBeVisible(); - expect($('.button-Facebook')).toBeVisible(); + expect($('.button-oa2-google-oauth2')).toBeVisible(); + expect($('.button-oa2-facebook')).toBeVisible(); }); it('displays a link to the password reset form', function() { diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js index 67c5a65f2a..ac9064e376 100644 --- a/lms/static/js/spec/student_account/register_spec.js +++ b/lms/static/js/spec/student_account/register_spec.js @@ -32,12 +32,14 @@ define([ currentProvider: null, providers: [ { + id: 'oa2-google-oauth2', name: 'Google', iconClass: 'fa-google-plus', loginUrl: '/auth/login/google-oauth2/?auth_entry=account_login', registerUrl: '/auth/login/google-oauth2/?auth_entry=account_register' }, { + id: 'oa2-facebook', name: 'Facebook', iconClass: 'fa-facebook', loginUrl: '/auth/login/facebook/?auth_entry=account_login', @@ -284,8 +286,8 @@ define([ createRegisterView(this); // Verify that Google and Facebook registration buttons are displayed - expect($('.button-Google')).toBeVisible(); - expect($('.button-Facebook')).toBeVisible(); + expect($('.button-oa2-google-oauth2')).toBeVisible(); + expect($('.button-oa2-facebook')).toBeVisible(); }); it('validates registration form fields', function() { diff --git a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js index 2c6e51caf3..c63915c780 100644 --- a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js +++ b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js @@ -29,7 +29,6 @@ define([ var createView = function( stepDataOverrides ) { var view = new MakePaymentStepView({ el: $( '#current-step-container' ), - templateName: 'make_payment_step', stepData: _.extend( _.clone( STEP_DATA ), stepDataOverrides ), errorModel: new ( Backbone.Model.extend({}) )() }).render(); diff --git a/lms/static/js/spec/verify_student/pay_and_verify_view_spec.js b/lms/static/js/spec/verify_student/pay_and_verify_view_spec.js index 0c94a54ef9..29fbf52b32 100644 --- a/lms/static/js/spec/verify_student/pay_and_verify_view_spec.js +++ b/lms/static/js/spec/verify_student/pay_and_verify_view_spec.js @@ -18,19 +18,16 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ]; var INTRO_STEP = { - templateName: "intro_step", name: "intro-step", title: "Intro" }; var DISPLAY_STEPS_FOR_PAYMENT = [ { - templateName: "make_payment_step", name: "make-payment-step", title: "Make Payment" }, { - templateName: "payment_confirmation_step", name: "payment-confirmation-step", title: "Payment Confirmation" } @@ -38,22 +35,18 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ var DISPLAY_STEPS_FOR_VERIFICATION = [ { - templateName: "face_photo_step", name: "face-photo-step", title: "Take Face Photo" }, { - templateName: "id_photo_step", name: "id-photo-step", title: "ID Photo" }, { - templateName: "review_photos_step", name: "review-photos-step", title: "Review Photos" }, { - templateName: "enrollment_confirmation_step", name: "enrollment-confirmation-step", title: "Enrollment Confirmation" } @@ -67,7 +60,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ }).render(); }; - var expectStepRendered = function( stepName, stepNum, numSteps ) { + var expectStepRendered = function( stepName ) { // Expect that the step container div rendered expect( $( '.' + stepName ).length > 0 ).toBe( true ); }; @@ -89,27 +82,27 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ); // Verify that the first step rendered - expectStepRendered('make-payment-step', 1, 6); + expectStepRendered('make-payment-step'); // Iterate through the steps, ensuring that each is rendered view.nextStep(); - expectStepRendered('payment-confirmation-step', 2, 6); + expectStepRendered('payment-confirmation-step'); view.nextStep(); - expectStepRendered('face-photo-step', 3, 6); + expectStepRendered('face-photo-step'); view.nextStep(); - expectStepRendered('id-photo-step', 4, 6); + expectStepRendered('id-photo-step'); view.nextStep(); - expectStepRendered('review-photos-step', 5, 6); + expectStepRendered('review-photos-step'); view.nextStep(); - expectStepRendered('enrollment-confirmation-step', 6, 6); + expectStepRendered('enrollment-confirmation-step'); // Going past the last step stays on the last step view.nextStep(); - expectStepRendered('enrollment-confirmation-step', 6, 6); + expectStepRendered('enrollment-confirmation-step'); }); it( 'renders intro and verification steps', function() { @@ -119,20 +112,20 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ); // Verify that the first step rendered - expectStepRendered('intro-step', 1, 5); + expectStepRendered('intro-step'); // Iterate through the steps, ensuring that each is rendered view.nextStep(); - expectStepRendered('face-photo-step', 2, 5); + expectStepRendered('face-photo-step'); view.nextStep(); - expectStepRendered('id-photo-step', 3, 5); + expectStepRendered('id-photo-step'); view.nextStep(); - expectStepRendered('review-photos-step', 4, 5); + expectStepRendered('review-photos-step'); view.nextStep(); - expectStepRendered('enrollment-confirmation-step', 5, 5); + expectStepRendered('enrollment-confirmation-step'); }); it( 'starts from a later step', function() { @@ -143,11 +136,11 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ); // Verify that we start on the right step - expectStepRendered('payment-confirmation-step', 2, 6); + expectStepRendered('payment-confirmation-step'); // Try moving to the next step view.nextStep(); - expectStepRendered('face-photo-step', 3, 6); + expectStepRendered('face-photo-step'); }); @@ -160,7 +153,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ // Jump back to the face photo step view.goToStep('face-photo-step'); - expectStepRendered('face-photo-step', 1, 4); + expectStepRendered('face-photo-step'); }); }); diff --git a/lms/static/js/spec/verify_student/reverify_view_spec.js b/lms/static/js/spec/verify_student/reverify_view_spec.js new file mode 100644 index 0000000000..52399d47c9 --- /dev/null +++ b/lms/static/js/spec/verify_student/reverify_view_spec.js @@ -0,0 +1,74 @@ +/** +* Tests for the reverification view. +**/ +define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/views/reverify_view'], + function( $, TemplateHelpers, ReverifyView ) { + 'use strict'; + + describe( 'edx.verify_student.ReverifyView', function() { + + var TEMPLATES = [ + "reverify", + "webcam_photo", + "image_input", + "error", + "face_photo_step", + "id_photo_step", + "review_photos_step", + "reverify_success_step" + ]; + + var STEP_INFO = { + 'face-photo-step': { + platformName: 'edX', + }, + 'id-photo-step': { + platformName: 'edX', + }, + 'review-photos-step': { + fullName: 'John Doe', + platformName: 'edX' + }, + 'reverify-success-step': { + platformName: 'edX' + } + }; + + var createView = function() { + return new ReverifyView({stepInfo: STEP_INFO}).render(); + }; + + var expectStepRendered = function( stepName ) { + // Expect that the step container div rendered + expect( $( '.' + stepName ).length > 0 ).toBe( true ); + }; + + + beforeEach(function() { + window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']); + + setFixtures('
      '); + $.each( TEMPLATES, function( index, templateName ) { + TemplateHelpers.installTemplate('templates/verify_student/' + templateName ); + }); + }); + + it( 'renders verification steps', function() { + var view = createView(); + + // Go through the flow, verifying that each step renders + // We rely on other unit tests to check the behavior of these subviews. + expectStepRendered('face-photo-step'); + + view.nextStep(); + expectStepRendered('id-photo-step'); + + view.nextStep(); + expectStepRendered('review-photos-step'); + + view.nextStep(); + expectStepRendered('reverify-success-step'); + }); + }); + } +); diff --git a/lms/static/js/spec/verify_student/review_photos_step_view_spec.js b/lms/static/js/spec/verify_student/review_photos_step_view_spec.js index 58d3757ab3..59f07447f1 100644 --- a/lms/static/js/spec/verify_student/review_photos_step_view_spec.js +++ b/lms/static/js/spec/verify_student/review_photos_step_view_spec.js @@ -21,7 +21,6 @@ define([ var createView = function() { return new ReviewPhotosStepView({ el: $( '#current-step-container' ), - templateName: 'review_photos_step', stepData: STEP_DATA, model: new VerificationModel({ faceImage: FACE_IMAGE, diff --git a/lms/static/js/sticky_footer.js b/lms/static/js/sticky_footer.js deleted file mode 100644 index 5f1e5a5458..0000000000 --- a/lms/static/js/sticky_footer.js +++ /dev/null @@ -1,23 +0,0 @@ -$(function() { - var stickyFooter = function(){ - var pageHeight = $('html').height(); - var windowHeight = $(window).height(); - var footerHeight = $('footer').outerHeight(); - - var totalHeight = $('footer').hasClass('fixed-bottom') ? pageHeight + footerHeight : pageHeight; - - - if (windowHeight < totalHeight) { - return $('footer').removeClass('fixed-bottom'); - } else { - return $('footer').addClass('fixed-bottom'); - } - }; - - stickyFooter(); - - $(window).resize(function() { - console.log("resizing"); - stickyFooter(); - }); -}); diff --git a/lms/static/js/student_account/accessApp.js b/lms/static/js/student_account/accessApp.js index 7c6579b536..2510bc1cff 100644 --- a/lms/static/js/student_account/accessApp.js +++ b/lms/static/js/student_account/accessApp.js @@ -11,6 +11,7 @@ var edx = edx || {}; return new edx.student.account.AccessView({ mode: container.data('initial-mode'), thirdPartyAuth: container.data('third-party-auth'), + thirdPartyAuthHint: container.data('third-party-auth-hint'), nextUrl: container.data('next-url'), platformName: container.data('platform-name'), loginFormDesc: container.data('login-form-desc'), diff --git a/lms/static/js/student_account/views/AccessView.js b/lms/static/js/student_account/views/AccessView.js index 9cf1dcae15..a909c61fae 100644 --- a/lms/static/js/student_account/views/AccessView.js +++ b/lms/static/js/student_account/views/AccessView.js @@ -18,7 +18,9 @@ var edx = edx || {}; subview: { login: {}, register: {}, - passwordHelp: {} + passwordHelp: {}, + institutionLogin: {}, + hintedLogin: {} }, nextUrl: '/dashboard', @@ -42,6 +44,8 @@ var edx = edx || {}; providers: [] }; + this.thirdPartyAuthHint = obj.thirdPartyAuthHint || null; + if (obj.nextUrl) { // Ensure that the next URL is internal for security reasons if ( ! window.isExternal( obj.nextUrl ) ) { @@ -52,7 +56,9 @@ var edx = edx || {}; this.formDescriptions = { login: obj.loginFormDesc, register: obj.registrationFormDesc, - reset: obj.passwordResetFormDesc + reset: obj.passwordResetFormDesc, + institution_login: null, + hinted_login: null }; this.platformName = obj.platformName; @@ -148,6 +154,26 @@ var edx = edx || {}; // Listen for 'auth-complete' event so we can enroll/redirect the user appropriately. this.listenTo( this.subview.register, 'auth-complete', this.authComplete ); + }, + + institution_login: function ( unused ) { + this.subview.institutionLogin = new edx.student.account.InstitutionLoginView({ + thirdPartyAuth: this.thirdPartyAuth, + platformName: this.platformName, + mode: this.activeForm + }); + + this.subview.institutionLogin.render(); + }, + + hinted_login: function ( unused ) { + this.subview.hintedLogin = new edx.student.account.HintedLoginView({ + thirdPartyAuth: this.thirdPartyAuth, + hintedProvider: this.thirdPartyAuthHint, + platformName: this.platformName + }); + + this.subview.hintedLogin.render(); } }, @@ -180,9 +206,11 @@ var edx = edx || {}; category: 'user-engagement' }); - if ( !this.form.isLoaded( $form ) ) { + // Load the form. Institution login is always refreshed since it changes based on the previous form. + if ( !this.form.isLoaded( $form ) || type == "institution_login") { this.loadForm( type ); } + this.activeForm = type; this.element.hide( $(this.el).find('.submission-success') ); this.element.hide( $(this.el).find('.form-wrapper') ); @@ -190,11 +218,13 @@ var edx = edx || {}; this.element.scrollTop( $anchor ); // Update url without reloading page - History.pushState( null, document.title, '/' + type + queryStr ); + if (type != "institution_login") { + History.pushState( null, document.title, '/' + type + queryStr ); + } analytics.page( 'login_and_registration', type ); // Focus on the form - document.getElementById(type).focus(); + $("#" + type).focus(); }, /** diff --git a/lms/static/js/student_account/views/FormView.js b/lms/static/js/student_account/views/FormView.js index 12f0d51100..989be0bc86 100644 --- a/lms/static/js/student_account/views/FormView.js +++ b/lms/static/js/student_account/views/FormView.js @@ -215,7 +215,9 @@ var edx = edx || {}; submitForm: function( event ) { var data = this.getFormData(); - event.preventDefault(); + if (!_.isUndefined(event)) { + event.preventDefault(); + } this.toggleDisableButton(true); diff --git a/lms/static/js/student_account/views/HintedLoginView.js b/lms/static/js/student_account/views/HintedLoginView.js new file mode 100644 index 0000000000..ae178e00ef --- /dev/null +++ b/lms/static/js/student_account/views/HintedLoginView.js @@ -0,0 +1,52 @@ +var edx = edx || {}; + +(function($, _, gettext) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.HintedLoginView = Backbone.View.extend({ + el: '#hinted-login-form', + + tpl: '#hinted_login-tpl', + + events: { + 'click .proceed-button': 'proceedWithHintedAuth' + }, + + formType: 'hinted-login', + + initialize: function( data ) { + this.tpl = $(this.tpl).html(); + this.providers = data.thirdPartyAuth.providers || []; + this.hintedProvider = _.findWhere(this.providers, {id: data.hintedProvider}) + this.platformName = data.platformName; + + }, + + render: function() { + $(this.el).html( _.template( this.tpl, { + // We pass the context object to the template so that + // we can perform variable interpolation using sprintf + providers: this.providers, + platformName: this.platformName, + hintedProvider: this.hintedProvider + })); + + return this; + }, + + proceedWithHintedAuth: function( event ) { + this.redirect(this.hintedProvider.loginUrl); + }, + + /** + * Redirect to a URL. Mainly useful for mocking out in tests. + * @param {string} url The URL to redirect to. + */ + redirect: function( url ) { + window.location.href = url; + } + }); +})(jQuery, _, gettext); diff --git a/lms/static/js/student_account/views/InstitutionLoginView.js b/lms/static/js/student_account/views/InstitutionLoginView.js new file mode 100644 index 0000000000..524e3a63b3 --- /dev/null +++ b/lms/static/js/student_account/views/InstitutionLoginView.js @@ -0,0 +1,30 @@ +var edx = edx || {}; + +(function($, _, Backbone) { + 'use strict'; + + edx.student = edx.student || {}; + edx.student.account = edx.student.account || {}; + + edx.student.account.InstitutionLoginView = Backbone.View.extend({ + el: '#institution_login-form', + + initialize: function( data ) { + var tpl = data.mode == "register" ? '#institution_register-tpl' : '#institution_login-tpl'; + this.tpl = $(tpl).html(); + this.providers = data.thirdPartyAuth.secondaryProviders || []; + this.platformName = data.platformName; + }, + + render: function() { + $(this.el).html( _.template( this.tpl, { + // We pass the context object to the template so that + // we can perform variable interpolation using sprintf + providers: this.providers, + platformName: this.platformName + })); + + return this; + } + }); +})(jQuery, _, Backbone); diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js index 79eb44d1ce..d54c65fddb 100644 --- a/lms/static/js/student_account/views/LoginView.js +++ b/lms/static/js/student_account/views/LoginView.js @@ -25,6 +25,9 @@ var edx = edx || {}; preRender: function( data ) { this.providers = data.thirdPartyAuth.providers || []; + this.hasSecondaryProviders = ( + data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length + ); this.currentProvider = data.thirdPartyAuth.currentProvider || ''; this.errorMessage = data.thirdPartyAuth.errorMessage || ''; this.platformName = data.platformName; @@ -45,6 +48,7 @@ var edx = edx || {}; currentProvider: this.currentProvider, errorMessage: this.errorMessage, providers: this.providers, + hasSecondaryProviders: this.hasSecondaryProviders, platformName: this.platformName } })); diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js index 177bfe51da..294704521b 100644 --- a/lms/static/js/student_account/views/RegisterView.js +++ b/lms/static/js/student_account/views/RegisterView.js @@ -22,9 +22,13 @@ var edx = edx || {}; preRender: function( data ) { this.providers = data.thirdPartyAuth.providers || []; + this.hasSecondaryProviders = ( + data.thirdPartyAuth.secondaryProviders && data.thirdPartyAuth.secondaryProviders.length + ); this.currentProvider = data.thirdPartyAuth.currentProvider || ''; this.errorMessage = data.thirdPartyAuth.errorMessage || ''; this.platformName = data.platformName; + this.autoSubmit = data.thirdPartyAuth.autoSubmitRegForm; this.listenTo( this.model, 'sync', this.saveSuccess ); }, @@ -41,12 +45,19 @@ var edx = edx || {}; currentProvider: this.currentProvider, errorMessage: this.errorMessage, providers: this.providers, + hasSecondaryProviders: this.hasSecondaryProviders, platformName: this.platformName } })); this.postRender(); + if (this.autoSubmit) { + $(this.el).hide(); + $('#register-honor_code').prop('checked', true); + this.submitForm(); + } + return this; }, @@ -63,6 +74,7 @@ var edx = edx || {}; }, saveError: function( error ) { + $(this.el).show(); // Show in case the form was hidden for auto-submission this.errors = _.flatten( _.map( JSON.parse(error.responseText), @@ -76,6 +88,13 @@ var edx = edx || {}; ); this.setErrors(); this.toggleDisableButton(false); - } + }, + + postFormSubmission: function() { + if (_.compact(this.errors).length) { + // The form did not get submitted due to validation errors. + $(this.el).show(); // Show in case the form was hidden for auto-submission + } + }, }); })(jQuery, _, gettext); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 07daa25174..1daf631906 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -137,7 +137,7 @@ screenReaderTitle: interpolate_text( gettext("Connect your {accountName} account"), {accountName: provider['name']} ), - valueAttribute: 'auth-' + provider.name.toLowerCase(), + valueAttribute: 'auth-' + provider.id, helpMessage: '', connected: provider.connected, connectUrl: provider.connect_url, diff --git a/lms/static/js/student_profile/views/learner_profile_factory.js b/lms/static/js/student_profile/views/learner_profile_factory.js index 033472f09e..372f5e52ac 100644 --- a/lms/static/js/student_profile/views/learner_profile_factory.js +++ b/lms/static/js/student_profile/views/learner_profile_factory.js @@ -78,6 +78,7 @@ new FieldsView.DropdownFieldView({ model: accountSettingsModel, screenReaderTitle: gettext('Country'), + titleVisible: false, required: true, editable: editable, showMessages: false, @@ -90,6 +91,7 @@ new AccountSettingsFieldViews.LanguageProficienciesFieldView({ model: accountSettingsModel, screenReaderTitle: gettext('Preferred Language'), + titleVisible: false, required: false, editable: editable, showMessages: false, diff --git a/lms/static/js/verify_student/pay_and_verify.js b/lms/static/js/verify_student/pay_and_verify.js index 0264d6d4c7..65d4781b2e 100644 --- a/lms/static/js/verify_student/pay_and_verify.js +++ b/lms/static/js/verify_student/pay_and_verify.js @@ -74,10 +74,12 @@ var edx = edx || {}; requirements: el.data('requirements') }, 'face-photo-step': { - platformName: el.data('platform-name') + platformName: el.data('platform-name'), + captureSoundPath: el.data('capture-sound') }, 'id-photo-step': { - platformName: el.data('platform-name') + platformName: el.data('platform-name'), + captureSoundPath: el.data('capture-sound') }, 'review-photos-step': { fullName: el.data('full-name'), diff --git a/lms/static/js/verify_student/photocapture.js b/lms/static/js/verify_student/photocapture.js deleted file mode 100644 index 7d3716238c..0000000000 --- a/lms/static/js/verify_student/photocapture.js +++ /dev/null @@ -1,356 +0,0 @@ -var onVideoFail = function(e) { - if(e == 'NO_DEVICES_FOUND') { - $('#no-webcam').show(); - $('#face_capture_button').hide(); - $('#photo_id_capture_button').hide(); - } - else { - console.log('Failed to get camera access!', e); - } -}; - -// Returns true if we are capable of video capture (regardless of whether the -// user has given permission). -function initVideoCapture() { - window.URL = window.URL || window.webkitURL; - navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || - navigator.mozGetUserMedia || navigator.msGetUserMedia; - return !(navigator.getUserMedia == undefined); -} - -var submitReverificationPhotos = function() { - // add photos to the form - $('').attr({ - type: 'hidden', - name: 'face_image', - value: $("#face_image")[0].src, - }).appendTo("#reverify_form"); - $('').attr({ - type: 'hidden', - name: 'photo_id_image', - value: $("#photo_id_image")[0].src, - }).appendTo("#reverify_form"); - - $("#reverify_form").submit(); - -} - -var submitMidcourseReverificationPhotos = function() { - $('').attr({ - type: 'hidden', - name: 'face_image', - value: $("#face_image")[0].src, - }).appendTo("#reverify_form"); - $("#reverify_form").submit(); -} - -function showSubmissionError() { - if (xhr.status == 400) { - $('#order-error .copy p').html(xhr.responseText); - } - $('#order-error').show(); - $("html, body").animate({ scrollTop: 0 }); -} - -function submitForm(data) { - for (prop in data) { - $('').attr({ - type: 'hidden', - name: prop, - value: data[prop] - }).appendTo('#pay_form'); - } - $("#pay_form").submit(); -} - -function refereshPageMessage() { - $('#photo-error').show(); - $("html, body").animate({ scrollTop: 0 }); -} - -var submitToPaymentProcessing = function() { - $(".payment-button").addClass('is-disabled').attr('aria-disabled', true); - var contribution_input = $("input[name='contribution']:checked") - var contribution = 0; - if(contribution_input.attr('id') == 'contribution-other') { - contribution = $("input[name='contribution-other-amt']").val(); - } - else { - contribution = contribution_input.val(); - } - var course_id = $("input[name='course_id']").val(); - $.ajax({ - url: "/verify_student/create_order", - type: 'POST', - data: { - "course_id" : course_id, - "contribution": contribution, - "face_image" : $("#face_image")[0].src, - "photo_id_image" : $("#photo_id_image")[0].src - }, - success:function(data) { - if (data.success) { - submitForm(data); - } else { - refereshPageMessage(); - } - }, - error:function(xhr,status,error) { - $(".payment-button").removeClass('is-disabled').attr('aria-disabled', false); - showSubmissionError() - } - }); -} - -function doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink) { - approveButton.removeClass('approved'); - nextButtonNav.addClass('is-not-ready'); - nextLink.attr('href', "#"); - - captureButton.show(); - resetButton.hide(); - approveButton.hide(); -} - -function doApproveButton(approveButton, nextButtonNav, nextLink) { - nextButtonNav.removeClass('is-not-ready'); - approveButton.addClass('approved'); - nextLink.attr('href', "#next"); -} - -function doSnapshotButton(captureButton, resetButton, approveButton) { - captureButton.hide(); - resetButton.show(); - approveButton.show(); -} - -function submitNameChange(event) { - event.preventDefault(); - $("#lean_overlay").fadeOut(200); - $("#edit-name").css({ 'display' : 'none' }); - var full_name = $('input[name="name"]').val(); - var xhr = $.post( - "/change_name", - { - "new_name" : full_name, - "rationale": "Want to match ID for ID Verified Certificates." - }, - function(data) { - $('#full-name').html(full_name); - } - ) - .fail(function(jqXhr,text_status, error_thrown) { - $('.message-copy').html(jqXhr.responseText); - }); - -} - -function initSnapshotHandler(names, hasHtml5CameraSupport) { - var name = names.pop(); - if (name == undefined) { - return; - } - - var video = $('#' + name + '_video'); - var canvas = $('#' + name + '_canvas'); - var image = $('#' + name + "_image"); - var captureButton = $("#" + name + "_capture_button"); - var resetButton = $("#" + name + "_reset_button"); - var approveButton = $("#" + name + "_approve_button"); - var nextButtonNav = $("#" + name + "_next_button_nav"); - var nextLink = $("#" + name + "_next_link"); - var flashCapture = $("#" + name + "_flash"); - - var ctx = null; - if (hasHtml5CameraSupport) { - ctx = canvas[0].getContext('2d'); - } - - var localMediaStream = null; - - function snapshot(event) { - if (hasHtml5CameraSupport) { - if (localMediaStream) { - ctx.drawImage(video[0], 0, 0); - image[0].src = canvas[0].toDataURL('image/png'); - } - else { - return false; - } - video[0].pause(); - } - else { - if (flashCapture[0].cameraAuthorized()) { - image[0].src = flashCapture[0].snap(); - } - else { - return false; - } - } - - doSnapshotButton(captureButton, resetButton, approveButton); - return false; - } - - function reset() { - image[0].src = ""; - - if (hasHtml5CameraSupport) { - video[0].play(); - } - else { - flashCapture[0].reset(); - } - - doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink); - return false; - } - - function approve() { - doApproveButton(approveButton, nextButtonNav, nextLink) - return false; - } - - // Initialize state for this picture taker - captureButton.show(); - resetButton.hide(); - approveButton.hide(); - nextButtonNav.addClass('is-not-ready'); - nextLink.attr('href', "#"); - - // Connect event handlers... - video.click(snapshot); - captureButton.click(snapshot); - resetButton.click(reset); - approveButton.click(approve); - - // If it's flash-based, we can just immediate initialize the next one. - // If it's HTML5 based, we have to do it in the callback from getUserMedia - // so that Firefox doesn't eat the second request. - if (hasHtml5CameraSupport) { - navigator.getUserMedia({video: true}, function(stream) { - video[0].src = window.URL.createObjectURL(stream); - localMediaStream = stream; - - // We do this in a recursive call on success because Firefox seems to - // simply eat the request if you stack up two on top of each other before - // the user has a chance to approve the first one. - // - // This appears to be necessary for older versions of Firefox (before 28). - // For more info, see https://github.com/edx/edx-platform/pull/3053 - initSnapshotHandler(names, hasHtml5CameraSupport); - }, onVideoFail); - } - else { - initSnapshotHandler(names, hasHtml5CameraSupport); - } - -} - -function browserHasFlash() { - var hasFlash = false; - try { - var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); - if(fo) hasFlash = true; - } catch(e) { - if(navigator.mimeTypes["application/x-shockwave-flash"] != undefined) hasFlash = true; - } - return hasFlash; -} - -function objectTagForFlashCamera(name) { - // detect whether or not flash is available - if(browserHasFlash()) { - // I manually update this to have ?v={2,3,4, etc} to avoid caching of flash - // objects on local dev. - return ''; - } - else { - // display a message informing the user to install flash - $('#no-flash').show(); - } -} - -function waitForFlashLoad(func, flash_object) { - if(!flash_object.hasOwnProperty('percentLoaded') || flash_object.percentLoaded() < 100){ - setTimeout(function() { - waitForFlashLoad(func, flash_object); - }, - 50); - } - else { - func(flash_object); - } -} - -$(document).ready(function() { - $(".carousel-nav").addClass('sr'); - $(".payment-button").click(function(){ - analytics.pageview("Payment Form"); - submitToPaymentProcessing(); - }); - - $("#reverify_button").click(function() { - submitReverificationPhotos(); - }); - - $("#midcourse_reverify_button").click(function() { - submitMidcourseReverificationPhotos(); - }); - - // prevent browsers from keeping this button checked - $("#confirm_pics_good").prop("checked", false) - $("#confirm_pics_good").change(function() { - $(".payment-button").toggleClass('disabled'); - $("#reverify_button").toggleClass('disabled'); - $("#midcourse_reverify_button").toggleClass('disabled'); - }); - - - // add in handlers to add/remove the correct classes to the body - // when moving between steps - $('#face_next_link').click(function(){ - analytics.pageview("Capture ID Photo"); - $('#photo-error').hide(); - $('body').addClass('step-photos-id').removeClass('step-photos-cam') - }) - - $('#photo_id_next_link').click(function(){ - analytics.pageview("Review Photos"); - $('body').addClass('step-review').removeClass('step-photos-id') - }) - - // set up edit information dialog - $('#edit-name div[role="alert"]').hide(); - $('#edit-name .action-save').click(submitNameChange); - - var hasHtml5CameraSupport = initVideoCapture(); - - // If HTML5 WebRTC capture is not supported, we initialize jpegcam - if (!hasHtml5CameraSupport) { - $("#face_capture_div").html(objectTagForFlashCamera("face_flash")); - $("#photo_id_capture_div").html(objectTagForFlashCamera("photo_id_flash")); - // wait for the flash object to be loaded and then check for a camera - if(browserHasFlash()) { - waitForFlashLoad(function(flash_object) { - if(!flash_object.hasOwnProperty('hasCamera')){ - onVideoFail('NO_DEVICES_FOUND'); - } - }, $('#face_flash')[0]); - } - } - - analytics.pageview("Capture Face Photo"); - initSnapshotHandler(["photo_id", "face"], hasHtml5CameraSupport); - - $('a[rel="external"]').attr({ - title: gettext('This link will open in a new browser window/tab'), - target: '_blank' - }); - -}); diff --git a/lms/static/js/verify_student/reverify.js b/lms/static/js/verify_student/reverify.js new file mode 100644 index 0000000000..42d732ae2b --- /dev/null +++ b/lms/static/js/verify_student/reverify.js @@ -0,0 +1,44 @@ +/** + * Reverification flow. + * + * This flow allows students who have a denied or expired verification + * to re-submit face and ID photos. It re-uses most of the same sub-views + * as the payment/verification flow. + */ + var edx = edx || {}; + + (function( $, _ ) { + 'use strict'; + var errorView, + el = $('#reverify-container'); + + edx.verify_student = edx.verify_student || {}; + + // Initialize an error view for displaying top-level error messages. + errorView = new edx.verify_student.ErrorView({ + el: $('#error-container') + }); + + // Initialize the base view, passing in information + // from the data attributes on the parent div. + return new edx.verify_student.ReverifyView({ + errorModel: errorView.model, + stepInfo: { + 'face-photo-step': { + platformName: el.data('platform-name'), + captureSoundPath: el.data('capture-sound') + }, + 'id-photo-step': { + platformName: el.data('platform-name'), + captureSoundPath: el.data('capture-sound') + }, + 'review-photos-step': { + fullName: el.data('full-name'), + platformName: el.data('platform-name') + }, + 'reverify-success-step': { + platformName: el.data('platform-name') + } + } + }).render(); +})( jQuery, _ ); diff --git a/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js b/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js index 6529402f91..f9402f56f2 100644 --- a/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js +++ b/lms/static/js/verify_student/views/enrollment_confirmation_step_view.js @@ -4,7 +4,7 @@ */ var edx = edx || {}; -(function( $ ) { +(function() { 'use strict'; edx.verify_student = edx.verify_student || {}; @@ -12,6 +12,9 @@ var edx = edx || {}; // Currently, this step does not need to install any event handlers, // since the displayed information is static. edx.verify_student.EnrollmentConfirmationStepView = edx.verify_student.StepView.extend({ + + templateName: 'enrollment_confirmation_step', + postRender: function() { // Track a virtual pageview, for easy funnel reconstruction. window.analytics.page( 'verification', this.templateName ); @@ -27,4 +30,4 @@ var edx = edx || {}; } }); -})( jQuery ); +})(); diff --git a/lms/static/js/verify_student/views/face_photo_step_view.js b/lms/static/js/verify_student/views/face_photo_step_view.js index c962eaab8e..b2654271b2 100644 --- a/lms/static/js/verify_student/views/face_photo_step_view.js +++ b/lms/static/js/verify_student/views/face_photo_step_view.js @@ -10,6 +10,8 @@ var edx = edx || {}; edx.verify_student.FacePhotoStepView = edx.verify_student.StepView.extend({ + templateName: "face_photo_step", + defaultContext: function() { return { platformName: '' @@ -22,7 +24,8 @@ var edx = edx || {}; model: this.model, modelAttribute: 'faceImage', submitButton: '#next_step_button', - errorModel: this.errorModel + errorModel: this.errorModel, + captureSoundPath: this.stepData.captureSoundPath }).render(); // Track a virtual pageview, for easy funnel reconstruction. diff --git a/lms/static/js/verify_student/views/id_photo_step_view.js b/lms/static/js/verify_student/views/id_photo_step_view.js index 289e772017..f89bd3e7ca 100644 --- a/lms/static/js/verify_student/views/id_photo_step_view.js +++ b/lms/static/js/verify_student/views/id_photo_step_view.js @@ -10,6 +10,8 @@ var edx = edx || {}; edx.verify_student.IDPhotoStepView = edx.verify_student.StepView.extend({ + templateName: "id_photo_step", + defaultContext: function() { return { platformName: '' @@ -22,7 +24,8 @@ var edx = edx || {}; model: this.model, modelAttribute: 'identificationImage', submitButton: '#next_step_button', - errorModel: this.errorModel + errorModel: this.errorModel, + captureSoundPath: this.stepData.captureSoundPath }).render(); // Track a virtual pageview, for easy funnel reconstruction. diff --git a/lms/static/js/verify_student/views/incourse_reverify_view.js b/lms/static/js/verify_student/views/incourse_reverify_view.js index c61bf9ddbb..1196816625 100644 --- a/lms/static/js/verify_student/views/incourse_reverify_view.js +++ b/lms/static/js/verify_student/views/incourse_reverify_view.js @@ -37,7 +37,6 @@ this.listenTo( this.model, 'sync', _.bind( this.handleSubmitPhotoSuccess, this )); this.listenTo( this.model, 'error', _.bind( this.handleSubmissionError, this )); - this.render(); }, render: function() { diff --git a/lms/static/js/verify_student/views/intro_step_view.js b/lms/static/js/verify_student/views/intro_step_view.js index eaa4de4eb2..dd0e75cbc6 100644 --- a/lms/static/js/verify_student/views/intro_step_view.js +++ b/lms/static/js/verify_student/views/intro_step_view.js @@ -10,6 +10,8 @@ var edx = edx || {}; edx.verify_student.IntroStepView = edx.verify_student.StepView.extend({ + templateName: "intro_step", + defaultContext: function() { return { introTitle: '', diff --git a/lms/static/js/verify_student/views/make_payment_step_view.js b/lms/static/js/verify_student/views/make_payment_step_view.js index 532534970b..20f56c03a9 100644 --- a/lms/static/js/verify_student/views/make_payment_step_view.js +++ b/lms/static/js/verify_student/views/make_payment_step_view.js @@ -10,6 +10,8 @@ var edx = edx || {}; edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({ + templateName: "make_payment_step", + defaultContext: function() { return { isActive: true, @@ -68,6 +70,9 @@ var edx = edx || {}; templateContext.requirements, function( isVisible ) { return isVisible; } ), + // This a hack to appease /lms/static/js/spec/verify_student/pay_and_verify_view_spec.js, + // which does not load an actual template context. + processors = templateContext.processors || [], self = this; // Track a virtual pageview, for easy funnel reconstruction. @@ -100,7 +105,7 @@ var edx = edx || {}; ); // create a button for each payment processor - _.each(templateContext.processors, function(processorName) { + _.each(processors.reverse(), function(processorName) { $( 'div.payment-buttons' ).append( self._getPaymentButtonHtml(processorName) ); }); diff --git a/lms/static/js/verify_student/views/pay_and_verify_view.js b/lms/static/js/verify_student/views/pay_and_verify_view.js index ee410ebacc..bad7d9e487 100644 --- a/lms/static/js/verify_student/views/pay_and_verify_view.js +++ b/lms/static/js/verify_student/views/pay_and_verify_view.js @@ -83,7 +83,6 @@ var edx = edx || {}; subviewConfig = { errorModel: this.errorModel, - templateName: this.displaySteps[i].templateName, nextStepTitle: nextStepTitle, stepData: stepData }; @@ -121,8 +120,6 @@ var edx = edx || {}; } // Render the subview - // Note that this will trigger a GET request for the - // underscore template. // When the view is rendered, it will overwrite the existing // step in the DOM. stepName = this.displaySteps[ this.currentStepIndex ].name; diff --git a/lms/static/js/verify_student/views/payment_confirmation_step_view.js b/lms/static/js/verify_student/views/payment_confirmation_step_view.js index ab662aa8f9..5e581dc7d7 100644 --- a/lms/static/js/verify_student/views/payment_confirmation_step_view.js +++ b/lms/static/js/verify_student/views/payment_confirmation_step_view.js @@ -10,6 +10,8 @@ var edx = edx || {}; edx.verify_student.PaymentConfirmationStepView = edx.verify_student.StepView.extend({ + templateName: "payment_confirmation_step", + defaultContext: function() { return { courseKey: '', diff --git a/lms/static/js/verify_student/views/reverify_success_step_view.js b/lms/static/js/verify_student/views/reverify_success_step_view.js new file mode 100644 index 0000000000..6d87d12eec --- /dev/null +++ b/lms/static/js/verify_student/views/reverify_success_step_view.js @@ -0,0 +1,17 @@ +/** + * Show a message to the student that he/she has successfully + * submitted photos for reverification. + */ + + var edx = edx || {}; + + (function() { + 'use strict'; + + edx.verify_student = edx.verify_student || {}; + + edx.verify_student.ReverifySuccessStepView = edx.verify_student.StepView.extend({ + templateName: 'reverify_success_step' + }); + + })(); diff --git a/lms/static/js/verify_student/views/reverify_view.js b/lms/static/js/verify_student/views/reverify_view.js new file mode 100644 index 0000000000..10547cddd9 --- /dev/null +++ b/lms/static/js/verify_student/views/reverify_view.js @@ -0,0 +1,115 @@ +/** + * Reverification flow. + * + * This flow allows students who have a denied or expired verification + * to re-submit face and ID photos. It re-uses most of the same sub-views + * as the payment/verification flow. + * + */ + + var edx = edx || {}; + +(function($, _, Backbone, gettext) { + 'use strict'; + + edx.verify_student = edx.verify_student || {}; + + edx.verify_student.ReverifyView = Backbone.View.extend({ + el: '#reverify-container', + + stepOrder: [ + "face-photo-step", + "id-photo-step", + "review-photos-step", + "reverify-success-step" + ], + stepViews: {}, + + initialize: function( obj ) { + this.errorModel = obj.errorModel || null; + this.initializeStepViews( obj.stepInfo || {} ); + this.currentStepIndex = 0; + }, + + initializeStepViews: function( stepInfo ) { + var verificationModel, stepViewConstructors, nextStepTitles; + + // We need to initialize this here, because + // outside of this method the subview classes + // might not yet have been loaded. + stepViewConstructors = { + 'face-photo-step': edx.verify_student.FacePhotoStepView, + 'id-photo-step': edx.verify_student.IDPhotoStepView, + 'review-photos-step': edx.verify_student.ReviewPhotosStepView, + 'reverify-success-step': edx.verify_student.ReverifySuccessStepView + }; + + nextStepTitles = [ + gettext( "Take a photo of your ID" ), + gettext( "Review your info" ), + gettext( "Confirm" ), + "" + ]; + + // Create the verification model, which is shared + // among the different steps. This allows + // one step to save photos and another step + // to submit them. + verificationModel = new edx.verify_student.VerificationModel(); + + _.each(this.stepOrder, function(name, index) { + var stepView = new stepViewConstructors[name]({ + errorModel: this.errorModel, + nextStepTitle: nextStepTitles[index], + stepData: stepInfo[name], + model: verificationModel + }); + + this.listenTo(stepView, 'next-step', this.nextStep); + this.listenTo(stepView, 'go-to-step', this.goToStep); + + this.stepViews[name] = stepView; + }, this); + }, + + render: function() { + this.renderCurrentStep(); + return this; + }, + + renderCurrentStep: function() { + var stepView, stepEl; + + // Get or create the step container + stepEl = $("#current-step-container"); + if (!stepEl.length) { + stepEl = $('
      ').appendTo(this.el); + } + + // Render the step subview + // When the view is rendered, it will overwrite the existing step in the DOM. + stepView = this.stepViews[ this.stepOrder[ this.currentStepIndex ] ]; + stepView.el = stepEl; + stepView.render(); + }, + + nextStep: function() { + this.currentStepIndex = Math.min( + this.currentStepIndex + 1, + this.stepOrder.length - 1 + ); + this.render(); + }, + + goToStep: function( stepName ) { + var stepIndex = _.indexOf(this.stepOrder, stepName); + + if ( stepIndex >= 0 ) { + this.currentStepIndex = stepIndex; + } + + this.render(); + } + }); + +})(jQuery, _, Backbone, gettext); diff --git a/lms/static/js/verify_student/views/review_photos_step_view.js b/lms/static/js/verify_student/views/review_photos_step_view.js index b4a329abb7..a83ed906ee 100644 --- a/lms/static/js/verify_student/views/review_photos_step_view.js +++ b/lms/static/js/verify_student/views/review_photos_step_view.js @@ -10,6 +10,8 @@ var edx = edx || {}; edx.verify_student.ReviewPhotosStepView = edx.verify_student.StepView.extend({ + templateName: "review_photos_step", + defaultContext: function() { return { platformName: '', diff --git a/lms/static/js/verify_student/views/webcam_photo_view.js b/lms/static/js/verify_student/views/webcam_photo_view.js index 835f571920..ea1c84a658 100644 --- a/lms/static/js/verify_student/views/webcam_photo_view.js +++ b/lms/static/js/verify_student/views/webcam_photo_view.js @@ -212,15 +212,11 @@ }, initialize: function( obj ) { - this.mainContainer = $('#pay-and-verify-container'); - if (!this.mainContainer){ - this.mainContainer = $('#incourse-reverify-container'); - } this.submitButton = obj.submitButton || ""; this.modelAttribute = obj.modelAttribute || ""; this.errorModel = obj.errorModel || null; this.backend = this.backends[obj.backendName] || obj.backend; - this.captureSoundPath = this.mainContainer.data('capture-sound'); + this.captureSoundPath = obj.captureSoundPath || ""; this.backend.initialize({ wrapper: "#camera", diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js index c6aa7639c8..35f11bd5db 100644 --- a/lms/static/js/views/fields.js +++ b/lms/static/js/views/fields.js @@ -325,6 +325,7 @@ mode: this.mode, title: this.options.title, screenReaderTitle: this.options.screenReaderTitle || this.options.title, + titleVisible: this.options.titleVisible || true, iconName: this.options.iconName, showBlankOption: (!this.options.required || !this.modelValueIsSet()), selectOptions: this.options.options, diff --git a/lms/static/sass/_developer.scss b/lms/static/sass/_developer.scss index 0964b4dcc8..7b5448dacf 100644 --- a/lms/static/sass/_developer.scss +++ b/lms/static/sass/_developer.scss @@ -69,7 +69,7 @@ @include margin-left( ($baseline/2) ); &.is-selected { - background: $m-blue-d3 !important; + background: $m-green-s1 !important; } } } diff --git a/lms/static/sass/base/_layouts.scss b/lms/static/sass/base/_layouts.scss index 6fa237f7b0..d4579becdc 100644 --- a/lms/static/sass/base/_layouts.scss +++ b/lms/static/sass/base/_layouts.scss @@ -57,7 +57,7 @@ body.view-incourse { .course-wrapper, .profile-wrapper { max-width: 1180px; - margin: 0 auto ($baseline*2) auto; + margin: 0 auto; padding: 0; } @@ -77,6 +77,7 @@ body.view-incourse { // site footer .wrapper-footer { + margin-top: $baseline; padding-right: 2%; padding-left: 2%; diff --git a/lms/static/sass/base/_reset.scss b/lms/static/sass/base/_reset.scss index bc39b2e7b5..78a8cb4393 100644 --- a/lms/static/sass/base/_reset.scss +++ b/lms/static/sass/base/_reset.scss @@ -84,7 +84,8 @@ td { vertical-align: top; } .clearfix { *zoom: 1; } @media print { - * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } + * { background: transparent; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } + html, body { background: transparent !important; } a, a:visited { text-decoration: underline; } abbr[title]:after { content: " (" attr(title) ")"; } .ir a:after { content: ""; } diff --git a/lms/static/sass/course/_info.scss b/lms/static/sass/course/_info.scss index 692a6dcabe..c67617bc9f 100644 --- a/lms/static/sass/course/_info.scss +++ b/lms/static/sass/course/_info.scss @@ -236,9 +236,14 @@ div.info-wrapper { } } } + + @media print { + background: transparent !important; + } } @media print { + background: transparent !important; border: 0; } } diff --git a/lms/static/sass/course/_profile.scss b/lms/static/sass/course/_profile.scss index 32ff94b641..e43f497777 100644 --- a/lms/static/sass/course/_profile.scss +++ b/lms/static/sass/course/_profile.scss @@ -215,11 +215,11 @@ border-bottom: 1px solid $lightGrey; padding: lh(0.5); > .requirement-name { - width: bi-app-invert-percentage(30%); + width: bi-app-invert-percentage(40%); display: inline-block; } > .requirement-status{ - width: bi-app-invert-percentage(70%); + width: bi-app-invert-percentage(60%); @include float(right); display: inline-block; .fa-times{ @@ -231,7 +231,7 @@ color: $success-color; } > .not-achieve{ - color: $lightGrey; + color: $darkGrey; } } } diff --git a/lms/static/sass/course/_tabs.scss b/lms/static/sass/course/_tabs.scss index d4a659833a..f16fc1df3d 100644 --- a/lms/static/sass/course/_tabs.scss +++ b/lms/static/sass/course/_tabs.scss @@ -12,5 +12,6 @@ div.static_tab_wrapper { @media print { border: 0; + background: transparent !important; } } \ No newline at end of file diff --git a/lms/static/sass/course/base/_extends.scss b/lms/static/sass/course/base/_extends.scss index cac78034be..19e065f31f 100644 --- a/lms/static/sass/course/base/_extends.scss +++ b/lms/static/sass/course/base/_extends.scss @@ -28,7 +28,7 @@ h1.top-header { .light-button, a.light-button, // only used in askbot as classes .gray-button { - @include button(simple, #eee); + @include button(simple, $gray-l5); @extend .button-reset; font-size: em(13); } diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 476d3f6e1f..9548715201 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -295,6 +295,7 @@ div.course-wrapper { @media print { padding: 0 2mm; + background: transparent !important; } } @@ -333,6 +334,7 @@ div.course-wrapper { @media print { border: 0; + background: transparent !important; } } diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index bc523891e7..4b01542e8c 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -193,4 +193,8 @@ header.global.slim { font-weight: bold; letter-spacing: 0; } + + @media print { + background: transparent !important; + } } diff --git a/lms/static/sass/multicourse/_account.scss b/lms/static/sass/multicourse/_account.scss index 8c50ae95c7..1eacb688e8 100644 --- a/lms/static/sass/multicourse/_account.scss +++ b/lms/static/sass/multicourse/_account.scss @@ -532,30 +532,30 @@ margin-right: 0; } - &.button-Google:hover, &.button-Google:focus { + &.button-oa2-google-oauth2:hover, &.button-oa2-google-oauth2:focus { background-color: #dd4b39; border: 1px solid #A5382B; } - &.button-Google:hover { + &.button-oa2-google-oauth2:hover { box-shadow: 0 2px 1px 0 #8D3024; } - &.button-Facebook:hover, &.button-Facebook:focus { + &.button-oa2-facebook:hover, &.button-oa2-facebook:focus { background-color: #3b5998; border: 1px solid #263A62; } - &.button-Facebook:hover { + &.button-oa2-facebook:hover { box-shadow: 0 2px 1px 0 #30487C; } - &.button-LinkedIn:hover , &.button-LinkedIn:focus { + &.button-oa2-linkedin-oauth2:hover , &.button-oa2-linkedin-oauth2:focus { background-color: #0077b5; border: 1px solid #06527D; } - &.button-LinkedIn:hover { + &.button-oa2-linkedin-oauth2:hover { box-shadow: 0 2px 1px 0 #005D8E; } diff --git a/lms/static/sass/multicourse/_courses.scss b/lms/static/sass/multicourse/_courses.scss index b1686dff19..d3f98320c9 100644 --- a/lms/static/sass/multicourse/_courses.scss +++ b/lms/static/sass/multicourse/_courses.scss @@ -133,7 +133,6 @@ $facet-background-color: #007db8; height: 120px; margin: 0 auto; max-width: 1200px; - min-width: 760px; padding-top: ($baseline*10); position: relative; text-align: center; diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 5edbdb4864..5d4e3ca26c 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -531,6 +531,10 @@ padding: 0; } + .purchase_credit { + float: right; + } + .message { @extend %ui-depth1; border-radius: 3px; diff --git a/lms/static/sass/multicourse/_home.scss b/lms/static/sass/multicourse/_home.scss index 2a452d45d0..cae7e48cb8 100644 --- a/lms/static/sass/multicourse/_home.scss +++ b/lms/static/sass/multicourse/_home.scss @@ -1,3 +1,6 @@ +@import '../base/grid-settings'; +@import 'neat/neat'; // lib - Neat + $title-left-margin: grid-width(2) + $gw-gutter; $button-size: ($baseline*2.75); $course-search-input-height: ($button-size); @@ -29,13 +32,12 @@ $course-search-input-height: ($button-size); position: relative; margin: 0 auto ($baseline); padding: ($baseline*5) ($baseline/2); - min-width: ($baseline*38); max-width: ($baseline*60); } .title { - @include margin-left($title-left-margin); - @include float(left); + @include span-columns(8); + @include shift(2); @include box-sizing(border-box); @include transition(all 0.2s linear 0s); position: relative; @@ -53,6 +55,24 @@ $course-search-input-height: ($button-size); } } + // 8 column layout + @include media($bp-medium) { + @include span-columns(6); + @include shift(1); + } + + // 4 column layout + @include media($bp-small) { + @include fill-parent(); + @include shift(0); + } + + // 4 column layout + @include media($bp-tiny) { + @include fill-parent(); + @include shift(0); + } + > hgroup { @include left(0); @include box-sizing(border-box); diff --git a/lms/static/sass/shared/_fields.scss b/lms/static/sass/shared/_fields.scss index 277278beee..b74ed55b28 100644 --- a/lms/static/sass/shared/_fields.scss +++ b/lms/static/sass/shared/_fields.scss @@ -127,7 +127,16 @@ display: none; } - &.mode-edit a.u-field-value-display { + button.u-field-value-display, button.u-field-value-display:active, button.u-field-value-display:focus, button.u-field-value-display:hover{ + border-color: transparent; + background: transparent; + padding: 0; + box-shadow: none; + font-size: inherit; + } + + + &.mode-edit button.u-field-value-display { display: none; } } diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index aac804bec2..46bbfb5bfb 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -13,9 +13,7 @@ footer#footer-openedx { @include clearfix(); @include box-sizing(border-box); - max-width: grid-width(12); - min-width: 760px; - width: flex-grid(12); + @include outer-container; margin: 0 auto; p, ol, ul { @@ -40,9 +38,16 @@ // colophon .colophon { - @include margin-right(flex-gutter()); - width: flex-grid(8,12); - @include float(left); + @include span-columns(8); + + @include media($bp-small) { + @include fill-parent(); + } + + @include media($bp-tiny) { + @include fill-parent(); + } + .nav-colophon { @include clearfix(); @@ -159,13 +164,20 @@ // platform Open edX logo and link .footer-about-openedx { - @include float(right); - width: flex-grid(3,12); - display: inline-block; - vertical-align: bottom; + @include span-columns(4); @include text-align(right); + vertical-align: bottom; + + @include media($bp-small) { + @include fill-parent(); + } + + @include media($bp-tiny) { + @include fill-parent(); + } a { + @include float(right); display: inline-block; &:hover { diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 91206f59b8..0d3b06d7bd 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -6,7 +6,6 @@ header.global { border-bottom: 1px solid $m-gray; box-shadow: 0 1px 5px 0 $shadow-l1; background: $header-bg; - height: 76px; position: relative; width: 100%; @@ -31,11 +30,9 @@ header.global { nav { @include clearfix(); - height: 40px; margin: 0 auto; - padding: 18px ($baseline/2) 0; + padding: 18px ($baseline/2) 12px; max-width: grid-width(12); - min-width: 760px; } .left { diff --git a/lms/static/sass/views/_decoupled-verification.scss b/lms/static/sass/views/_decoupled-verification.scss index 6a38eb93dc..95d69833b9 100644 --- a/lms/static/sass/views/_decoupled-verification.scss +++ b/lms/static/sass/views/_decoupled-verification.scss @@ -1,6 +1,6 @@ // Updates for decoupled verification A/B test .verification-process { - .pay-and-verify, .incourse-reverify { + .pay-and-verify, .incourse-reverify, .reverify { .review { .title.center-col { padding: 0 calc( ( 100% - 750px ) / 2 ) 10px; diff --git a/lms/static/sass/views/_login-register.scss b/lms/static/sass/views/_login-register.scss index 81d43ee80d..aad4cab6f3 100644 --- a/lms/static/sass/views/_login-register.scss +++ b/lms/static/sass/views/_login-register.scss @@ -14,6 +14,7 @@ $sm-btn-linkedin: #0077b5; background: $white; min-height: 100%; width: 100%; + $third-party-button-height: ($baseline*1.75); h2 { @extend %t-title5; @@ -22,6 +23,10 @@ $sm-btn-linkedin: #0077b5; font-family: $sans-serif; } + .instructions { + @extend %t-copy-base; + } + /* Temp. fix until applied globally */ > { @include box-sizing(border-box); @@ -67,10 +72,11 @@ $sm-btn-linkedin: #0077b5; } } - form { + form, + .wrapper-other-login { border: 1px solid $gray-l4; - border-radius: 5px; - padding: 0px 25px 20px 25px; + border-radius: ($baseline/4); + padding: 0 ($baseline*1.25) $baseline ($baseline*1.25); } .section-title { @@ -106,16 +112,20 @@ $sm-btn-linkedin: #0077b5; } } - .nav-btn { + %nav-btn-base { @extend %btn-secondary-blue-outline; width: 100%; height: ($baseline*2); text-transform: none; text-shadow: none; - font-weight: 600; letter-spacing: normal; } + .nav-btn { + @extend %nav-btn-base; + @extend %t-strong; + } + .form-type, .toggle-form { @include box-sizing(border-box); @@ -348,29 +358,31 @@ $sm-btn-linkedin: #0077b5; .login-provider { @extend %btn-secondary-grey-outline; - width: 130px; - padding: 0 0 0 ($baseline*2); - height: 34px; - text-align: left; + @extend %t-action4; + + @include padding(0, 0, 0, $baseline*2); + @include text-align(left); + + position: relative; + margin-right: ($baseline/4); + margin-bottom: $baseline; + border-color: $lightGrey1; + width: $baseline*6.5; + height: $third-party-button-height; text-shadow: none; text-transform: none; - position: relative; - font-size: 0.8em; - border-color: $lightGrey1; - - &:nth-of-type(odd) { - margin-right: 13px; - } .icon { - color: white; + @include left(0); + position: absolute; top: -1px; - left: 0; width: 30px; - height: 34px; - line-height: 34px; + bottom: -1px; + background: $m-blue-d3; + line-height: $third-party-button-height; text-align: center; + color: $white; } &:hover, @@ -378,17 +390,13 @@ $sm-btn-linkedin: #0077b5; background-image: none; .icon { - height: 32px; - line-height: 32px; top: 0; + bottom: 0; + line-height: ($third-party-button-height - 2px); } } - &:last-child { - margin-bottom: $baseline; - } - - &.button-Google { + &.button-oa2-google-oauth2 { color: $sm-btn-google; .icon { @@ -407,7 +415,7 @@ $sm-btn-linkedin: #0077b5; } } - &.button-Facebook { + &.button-oa2-facebook { color: $sm-btn-facebook; .icon { @@ -426,7 +434,7 @@ $sm-btn-linkedin: #0077b5; } } - &.button-LinkedIn { + &.button-oa2-linkedin-oauth2 { color: $sm-btn-linkedin; .icon { @@ -447,6 +455,19 @@ $sm-btn-linkedin: #0077b5; } + .button-secondary-login { + @extend %nav-btn-base; + @extend %t-action4; + @extend %t-regular; + border-color: $lightGrey1; + padding: 0; + height: $third-party-button-height; + + &:hover { + border-color: $m-blue-d3; + } + } + /** Error Container - from _account.scss **/ .status { @include box-sizing(border-box); @@ -503,6 +524,13 @@ $sm-btn-linkedin: #0077b5; } } + .institution-list { + + .institution { + @extend %t-copy-base; + } + } + @include media( max-width 330px) { .form-type { width: 98%; diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index b9a00db98c..03d939f3ca 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -53,8 +53,7 @@ // ==================== // VIEW: all verification steps -.verification-process, -.midcourse-reverification-process { +.verification-process { // reset: box-sizing (making things so right its scary) * { @@ -114,6 +113,11 @@ padding-bottom: ($baseline +1); } + // HACK: fix global header height in verification flow ECOM-1808 + header.global { + height: 76px; + } + // HACK: nasty override due to our bad input/button styling button, input[type="submit"], input[type="button"], button[type="submit"] { @include font-size(16); @@ -165,7 +169,7 @@ // elements - controls .action-primary { - @extend %btn-primary-blue; + @extend %btn-verify-primary; // needed for override due to .register a:link styling border: 0 !important; color: $white !important; @@ -555,38 +559,6 @@ // ==================== - // UI: reverification message - .wrapper-reverification { - border-bottom: ($baseline/10) solid $m-pink; - margin-bottom: $baseline; - padding-bottom: $baseline; - position: relative; - - .deco-arrow { - @include triangle($baseline, $m-pink, down); - position: absolute; - bottom: -($baseline); - @include left(50%); - } - } - - .reverification { - - .message { - - .title { - @extend %hd-lv3; - color: $m-pink; - } - - .copy { - @extend %t-copy-sub1; - } - } - } - - // ==================== - // UI: slides .carousel { @@ -1965,441 +1937,6 @@ } } - } - - // VIEW: midcourse re-verification - &.midcourse-reverification-process { - - // step-dash - - &.step-dash { - - .content-main > .title { - @extend %t-title7; - display: block; - font-weight: 600; - color: $m-gray; - } - - .wrapper-reverify-open, - .wrapper-reverify-status { - display: inline-block; - vertical-align: top; - width: 48%; - } - - .copy .title { - @extend %t-title6; - font-weight: 600; - } - - .wrapper-reverify-status .title { - @extend %t-title6; - font-weight: normal; - color: $m-gray; - } - - .action-reverify { - padding: ($baseline/2) ($baseline*0.75); - } - - .reverification-list { - @include margin-right($baseline*1.5); - padding: 0; - list-style-type: none; - - .item { - box-shadow: 0 2px 5px 0 $shadow-l1 inset; - @include margin(($baseline*0.75), ($baseline*0.75), ($baseline*0.75), 0); - border: 1px solid $m-gray-t2; - - &.complete { - border: 1px solid $success-color; - - .course-info { - opacity: .5; - - .course-name { - font-weight: normal; - } - } - - .reverify-status { - @extend %t-weight4; - border-top: 1px solid $light-gray; - background-color: $m-gray-l4; - color: $success-color; - } - } - - &.pending { - border: 1px solid $warning-color; - - .course-info { - opacity: .5; - - .course-name { - font-weight: normal; - } - } - - .reverify-status { - @extend %t-weight4; - border-top: 1px solid $light-gray; - background-color: $m-gray-l4; - color: $warning-color; - } - } - - &.failed { - border: 1px solid $alert-color; - - .course-info { - opacity: .5; - - .course-name { - font-weight: normal; - } - } - - .reverify-status { - @extend %t-weight4; - border-top: 1px solid $light-gray; - background-color: $m-gray-l4; - color: $alert-color; - } - } - } - - .course-info { - margin-bottom: ($baseline/2); - padding: ($baseline/2) ($baseline*0.75); - } - - .course-name { - @extend %t-title5; - display: block; - font-weight: bold; - } - - .deadline { - @extend %copy-detail; - display: block; - margin-top: ($baseline/4); - } - - .reverify-status { - background-color: $light-gray; - padding: ($baseline/2) ($baseline*0.75); - } - } - - .support { - margin-top: $baseline; - @extend %t-copy-sub1; - } - - .wrapper-reverification-help { - margin-top: $baseline; - border-top: 1px solid $light-gray; - padding-top: ($baseline*1.5); - - .faq-item { - display: inline-block; - vertical-align: top; - width: flex-grid(4,12); - @include padding-right($baseline); - - &:last-child { - @include padding-right(0); - } - - .faq-answer { - @extend %t-copy-sub1; - } - } - } - } - - // step-photos - &.step-photos { - - .block-photo .title { - @extend %t-title4; - color: $m-blue-d1; - } - - .wrapper-task { - @include clearfix(); - width: flex-grid(12,12); - margin: $baseline 0; - - .wrapper-help { - @include float(right); - width: flex-grid(6,12); - padding: 0 $baseline; - - .help { - margin-bottom: ($baseline*1.5); - - &:last-child { - margin-bottom: 0; - } - - .title { - @extend %hd-lv3; - } - - .copy { - @extend %copy-detail; - } - - .example { - color: $m-gray-l2; - } - - // help - general list - .list-help { - margin-top: ($baseline/2); - color: $black; - - .help-item { - margin-bottom: ($baseline/4); - border-bottom: 1px solid $m-gray-l4; - padding-bottom: ($baseline/4); - - &:last-child { - margin-bottom: 0; - border-bottom: none; - padding-bottom: 0; - } - } - - .help-item-emphasis { - @extend %t-weight4; - } - } - - // help - faq - .list-faq { - margin-bottom: $baseline; - } - } - } - - .task { - @extend %ui-window; - @include float(left); - @include margin-right(flex-gutter()); - width: flex-grid(6,12); - } - - .controls { - padding: ($baseline*0.75) $baseline; - background: $m-gray-l4; - - .list-controls { - position: relative; - } - - .control { - position: absolute; - - .action { - @extend %btn-primary-blue; - padding: ($baseline/2) ($baseline*0.75); - - .icon { - @extend %t-icon4; - padding: ($baseline*.25) ($baseline*.5); - display: block; - } - } - - // STATE: hidden - &.is-hidden { - visibility: hidden; - } - - // STATE: shown - &.is-shown { - visibility: visible; - } - - // STATE: approved - &.approved { - - .action { - @extend %btn-verify-primary; - padding: ($baseline/2) ($baseline*0.75); - } - } - } - - // control - redo - .control-redo { - position: absolute; - @include left($baseline/2); - } - - // control - take/do, retake - .control-do, .control-retake { - @include left(45%); - } - - // control - approve - .control-approve { - position: absolute; - @include right($baseline/2); - } - } - - .msg { - @include clearfix(); - margin-top: ($baseline*2); - - .copy { - @include float(left); - width: flex-grid(8,12); - @include margin-right(flex-gutter()); - } - - .list-actions { - position: relative; - top: -($baseline/2); - @include float(left); - width: flex-grid(4,12); - @include text-align(right); - - .action-retakephotos a { - @extend %btn-primary-blue; - @include font-size(14); - padding: ($baseline/2) ($baseline*0.75); - } - } - } - - .msg-followup { - border-top: ($baseline/10) solid $m-gray-t0; - padding-top: $baseline; - } - } - - .review-task { - margin-bottom: ($baseline*1.5); - padding: ($baseline*0.75) $baseline; - border-radius: ($baseline/10); - background: $m-gray-l4; - - &:last-child { - margin-bottom: 0; - } - - > .title { - @extend %hd-lv3; - } - - .copy { - @extend %copy-base; - - strong { - @extend %t-weight5; - color: $m-gray-d4; - } - } - } - - - // individual task - name - .review-task-name { - @include clearfix(); - border: 1px solid $light-gray; - - .copy { - @include float(left); - width: flex-grid(8,12); - @include margin-right(flex-gutter()); - } - - .list-actions { - position: relative; - top: -($baseline); - @include float(left); - width: flex-grid(4,12); - @include text-align(right); - - .action-editname a { - @extend %btn-primary-blue; - @include font-size(14); - padding: ($baseline/2) ($baseline*0.75); - } - } - } - - .nav-wizard { - padding: ($baseline*0.75) $baseline; - - .prompt-verify { - @include float(left); - @include margin(0, flex-gutter(), 0, 0); - width: flex-grid(6,12); - - .title { - @extend %hd-lv4; - margin-bottom: ($baseline/4); - } - - .copy { - @extend %t-copy-sub1; - @extend %t-weight3; - } - - .list-actions { - margin-top: ($baseline/2); - } - - .action-verify label { - @extend %t-copy-sub1; - } - } - - .wizard-steps { - margin-top: ($baseline/2); - - .wizard-step { - @include margin-right(flex-gutter()); - display: inline-block; - vertical-align: middle; - - &:last-child { - @include margin-right(0); - } - } - } - } - - - .modal { - - fieldset { - margin-top: $baseline; - } - - .close-modal { - @include font-size(24); - color: $m-blue-d3; - - &:hover, &:focus { - color: $m-blue-d1; - border: none; - } - } - } - - } - } - - &.step-confirmation { .instruction { display: inline-block; @@ -2425,10 +1962,39 @@ margin: $baseline 0; } } + } + .reverify-success-step { + .title { + @extend %t-title4; + text-align: left; + text-transform: none; + } + + .wrapper-actions { + margin-top: 20px; + } } } +.reverify-blocked { + + @include padding(($baseline*1.5), ($baseline*1.5), ($baseline*2), ($baseline*1.5)); + + .title { + @extend %t-title4; + text-align: left; + text-transform: none; + } + + .wrapper-actions { + margin-top: 20px; + } + + .action-primary { + @extend %btn-primary-blue; + } +} //reverify notification special styles .msg-reverify { @@ -2437,13 +2003,6 @@ } } -// UI: photo reverification heading -h2.photo_verification { - @extend %t-title1; - text-align: left; - text-transform: none; -} - .facephoto.view { .wrapper-task { #facecam { diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index ce6e5e8d41..5ceac475e1 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -3,7 +3,7 @@ <%! from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from util.date_utils import get_time_display, DEFAULT_LONG_DATE_FORMAT +from util.date_utils import get_time_display, DEFAULT_SHORT_DATE_FORMAT from django.conf import settings from django.utils.http import urlquote_plus %> @@ -103,26 +103,27 @@ from django.utils.http import urlquote_plus %endif - %if credit_course is not None: + % if credit_course_requirements:

      ${_("Requirements for Course Credit")}

      - %if credit_course['eligibility_status'] == 'not_eligible': - ${student.username}, ${_("You are no longer eligible for this course.")} - %elif credit_course['eligibility_status'] == 'eligible': - ${student.username}, ${_("You have met the requirements for credit in this course.")} - ${_("Go to your dashboard")} ${_("to purchase course credit.")} + %if credit_course_requirements['eligibility_status'] == 'not_eligible': + ${student.get_full_name()}, ${_("You are no longer eligible for this course.")} + %elif credit_course_requirements['eligibility_status'] == 'eligible': + ${student.get_full_name()}, ${_("You have met the requirements for credit in this course.")} + ${_("{link} to purchase course credit.").format(link="{url_name}".format(url = reverse('dashboard'), url_name = _('Go to your dashboard')))} - %elif credit_course['eligibility_status'] == 'partial_eligible': - ${student.username}, ${_("You have not yet met the requirements for credit.")} + %elif credit_course_requirements['eligibility_status'] == 'partial_eligible': + ${student.get_full_name()}, ${_("You have not yet met the requirements for credit.")} %endif
      -
      diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 3dc379fab6..821a1291f5 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -83,7 +83,8 @@ from django.core.urlresolvers import reverse <% is_course_blocked = (course.id in block_courses) %> <% course_verification_status = verification_status_by_course.get(course.id, {}) %> <% course_requirements = courses_requirements_not_met.get(course.id) %> - <%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings" /> + <% credit_message = credit_messages.get(unicode(course.id)) %> + <%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, credit_message=credit_message, user=user" /> % endfor % if settings.FEATURES.get('CUSTOM_COURSES_EDX', False): diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index bf44791adc..aa431bbf0b 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -1,4 +1,4 @@ -<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings" /> +<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, credit_message" /> <%! import urllib @@ -273,7 +273,11 @@ from student.helpers import (
        % if course.may_certify() and cert_status: <%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/> - % endif + % endif + + % if credit_message: + <%include file='_dashboard_credit_information.html' args='credit_message=credit_message'/> + % endif % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY] and not is_course_blocked:
        diff --git a/lms/templates/dashboard/_dashboard_credit_information.html b/lms/templates/dashboard/_dashboard_credit_information.html new file mode 100644 index 0000000000..efdb39337b --- /dev/null +++ b/lms/templates/dashboard/_dashboard_credit_information.html @@ -0,0 +1,70 @@ +<%page args="credit_message" /> + +<%! +from django.utils.translation import ugettext as _ +from course_modes.models import CourseMode +from util.date_utils import get_default_time_display +%> +<%namespace name='static' file='../static_content.html'/> + +<%block name="js_extra" args="credit_message"> + <%static:js group='credit_wv'/> + + + +
        +

        + % if credit_message["status"] == "requirements_meet": + + % if credit_message["urgent"]: + ${_("{username}, your eligibility for credit expires on {expiry}. Don't miss out!").format( + username=credit_message["user_full_name"], + expiry=get_default_time_display(credit_message["expiry"]) + ) + } + % else: + ${_("{congrats} {username}, You have meet requirements for credit.").format( + congrats="Congratulations", + username=credit_message["user_full_name"] + ) + } + % endif + + ${_("Purchase Credit")} + + % elif credit_message["status"] == "pending": + ${_("Thank you, your payment is complete, your credit is processing. Please see {provider_link} for more information.").format( + provider_link='{}'.format(credit_message["provider"]["display_name"]) + ) + } + % elif credit_message["status"] == "approved": + ${_("Thank you, your credit is approved. Please see {provider_link} for more information.").format( + provider_link='{}'.format(credit_message["provider"]["display_name"]) + ) + } + % elif credit_message["status"] == "rejected": + ${_("Your credit has been denied. Please contact {provider_link} for more information.").format( + provider_link='{}'.format(credit_message["provider"]["display_name"]) + ) + } + % endif + +

        + +
        diff --git a/lms/templates/dashboard/_dashboard_third_party_error.html b/lms/templates/dashboard/_dashboard_third_party_error.html index 99ba0ae4fb..a7958b9481 100644 --- a/lms/templates/dashboard/_dashboard_third_party_error.html +++ b/lms/templates/dashboard/_dashboard_third_party_error.html @@ -5,7 +5,7 @@

        ${_("Could Not Link Accounts")}

        ## Translators: this message is displayed when a user tries to link their account with a third-party authentication provider (for example, Google or LinkedIn) with a given edX account, but their third-party account is already associated with another edX account. provider_name is the name of the third-party authentication provider, and platform_name is the name of the edX deployment. -

        ${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name='{duplicate_provider}'.format(duplicate_provider=duplicate_provider.NAME), platform_name=platform_name)}

        +

        ${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name=duplicate_provider, platform_name=platform_name)}

      diff --git a/lms/templates/fields/field_dropdown.underscore b/lms/templates/fields/field_dropdown.underscore index b42ab0cc36..6ec92412a3 100644 --- a/lms/templates/fields/field_dropdown.underscore +++ b/lms/templates/fields/field_dropdown.underscore @@ -4,6 +4,12 @@ <% } %> +<% if (!titleVisible) { %> + +<% } %> + <% if (iconName) { %> <% } %> @@ -17,11 +23,11 @@ <% }); %> - - <%- screenReaderTitle %> +
      diff --git a/lms/templates/login.html b/lms/templates/login.html index ed666adf51..c6483df2a0 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -130,6 +130,8 @@ from microsite_configuration import microsite +<%include file="forgot_password_modal.html" /> +

      @@ -219,7 +221,7 @@ from microsite_configuration import microsite % for enabled in provider.Registry.enabled(): ## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn). - + % endfor @@ -239,4 +241,3 @@ from microsite_configuration import microsite

      - diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html index e99a40d531..d678e36055 100644 --- a/lms/templates/navigation-edx.html +++ b/lms/templates/navigation-edx.html @@ -150,8 +150,4 @@ site_status_msg = get_site_status_msg(course_id) % endif -%if not user.is_authenticated(): - <%include file="forgot_password_modal.html" /> -%endif - <%include file="help_modal.html"/> diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index f925bfee85..2ce84db750 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -156,8 +156,4 @@ site_status_msg = get_site_status_msg(course_id) % endif -%if not user.is_authenticated(): - <%include file="forgot_password_modal.html" /> -%endif - <%include file="help_modal.html"/> diff --git a/lms/templates/register.html b/lms/templates/register.html index f885769658..c913be8466 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -132,7 +132,7 @@ import calendar % for enabled in provider.Registry.enabled(): ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). - + % endfor diff --git a/lms/templates/student_account/access.underscore b/lms/templates/student_account/access.underscore index 2eee3a2a3d..a2bc97030f 100644 --- a/lms/templates/student_account/access.underscore +++ b/lms/templates/student_account/access.underscore @@ -9,3 +9,11 @@
      + +
      + +
      + +
      +
      +
      diff --git a/lms/templates/student_account/hinted_login.underscore b/lms/templates/student_account/hinted_login.underscore new file mode 100644 index 0000000000..d1cb0d8379 --- /dev/null +++ b/lms/templates/student_account/hinted_login.underscore @@ -0,0 +1,24 @@ + diff --git a/lms/templates/student_account/institution_login.underscore b/lms/templates/student_account/institution_login.underscore new file mode 100644 index 0000000000..88861616e2 --- /dev/null +++ b/lms/templates/student_account/institution_login.underscore @@ -0,0 +1,31 @@ + diff --git a/lms/templates/student_account/institution_register.underscore b/lms/templates/student_account/institution_register.underscore new file mode 100644 index 0000000000..ba97dd6e7e --- /dev/null +++ b/lms/templates/student_account/institution_register.underscore @@ -0,0 +1,31 @@ + diff --git a/lms/templates/student_account/login.underscore b/lms/templates/student_account/login.underscore index 5420e769c4..58447bdba8 100644 --- a/lms/templates/student_account/login.underscore +++ b/lms/templates/student_account/login.underscore @@ -39,7 +39,7 @@ - <% if ( context.providers.length > 0 && !context.currentProvider ) { %> + <% if ( context.providers.length > 0 && !context.currentProvider || context.hasSecondaryProviders ) { %> - <% } else if ( context.providers.length > 0 ) { %> + <% } else if ( context.providers.length > 0 || context.hasSecondaryProviders ) { %>