diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d0fde72ac11d..5345413e561c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,3 +7,15 @@ updates: interval: "weekly" reviewers: - "openedx/arbi-bom" + - package-ecosystem: "github-actions" + directory: "/.github/actions/unit-tests/" + schedule: + interval: "weekly" + reviewers: + - "openedx/arbi-bom" + - package-ecosystem: "github-actions" + directory: "/.github/actions/verify-tests-count/" + schedule: + interval: "weekly" + reviewers: + - "openedx/arbi-bom" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bc8c2bbab21c..e284142d9c04 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,8 +6,6 @@ 🌳🌳🌳🌳 or ask in the #wg-build-test-release Slack channel if you have any questions or need help. 🌳🌳 -🌴🌴🌴🌴🌴🌴 🌴 Note: the Palm release is still supported. - Please consider whether your change should be applied to Palm as well. Please give your pull request a short but descriptive title. Use conventional commits to separate and summarize commits logically: diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index ff73e14fce53..978e616ee62a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -30,7 +30,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} diff --git a/.github/workflows/js-tests.yml b/.github/workflows/js-tests.yml index d28f6da24ff9..2ca5f8d45e69 100644 --- a/.github/workflows/js-tests.yml +++ b/.github/workflows/js-tests.yml @@ -23,7 +23,7 @@ jobs: run: git fetch --depth=1 origin master - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/pylint-checks.yml b/.github/workflows/pylint-checks.yml index a67dcf83e3ac..840dc985e3c2 100644 --- a/.github/workflows/pylint-checks.yml +++ b/.github/workflows/pylint-checks.yml @@ -16,7 +16,7 @@ jobs: - module-name: lms-1 path: "--django-settings-module=lms.envs.test lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/" - module-name: lms-2 - path: "--django-settings-module=lms.envs.test lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/learner_recommendations/ lms/djangoapps/learner_home/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/djangoapps/mfe_config_api/ lms/envs/ lms/lib/ lms/tests.py" + path: "--django-settings-module=lms.envs.test lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/learner_home/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/djangoapps/mfe_config_api/ lms/envs/ lms/lib/ lms/tests.py" - module-name: openedx-1 path: "--django-settings-module=lms.envs.test openedx/core/types/ openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/content_staging/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/demographics/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/tests/ openedx/core/djangoapps/course_live/" - module-name: openedx-2 diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 0097feb4cd37..a5e65fb67e94 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -35,7 +35,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/static-assets-check.yml b/.github/workflows/static-assets-check.yml index 271b6ada81ee..37c70a5c1362 100644 --- a/.github/workflows/static-assets-check.yml +++ b/.github/workflows/static-assets-check.yml @@ -32,7 +32,7 @@ jobs: sudo apt-get install libxmlsec1-dev pkg-config - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 4c95f2510fa1..3afd691daf58 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -53,7 +53,6 @@ "lms/djangoapps/instructor_task/", "lms/djangoapps/learner_dashboard/", "lms/djangoapps/learner_home/", - "lms/djangoapps/learner_recommendations/", "lms/djangoapps/lms_initialization/", "lms/djangoapps/lms_xblock/", "lms/djangoapps/lti_provider/", diff --git a/.gitignore b/.gitignore index 169296690009..cc568a924c52 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,8 @@ conf/locale/fake*/LC_MESSAGES/*.po conf/locale/fake*/LC_MESSAGES/*.mo # this was a mistake in i18n_tools, now fixed. conf/locale/messages.mo +conf/plugins-locale/ +/*/static/js/xblock.v1-i18n/ ### Testing artifacts .testids/ diff --git a/Dockerfile b/Dockerfile index a159d32464d3..f4eba6fd66a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -147,6 +147,9 @@ COPY . . # Install Python requirements again in order to capture local projects RUN pip install -e . +# Setting edx-platform directory as safe for git commands +RUN git config --global --add safe.directory /edx/app/edxapp/edx-platform + # Production target FROM base as production diff --git a/Makefile b/Makefile index 55252bf2aff0..cc6fa5591376 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,8 @@ docker_auth docker_build docker_tag_build_push_lms docker_tag_build_push_lms_dev \ docker_tag_build_push_cms docker_tag_build_push_cms_dev docs extract_translations \ guides help lint-imports local-requirements migrate migrate-lms migrate-cms \ - pre-requirements pull pull_translations push_translations requirements shell swagger \ + pre-requirements pull pull_xblock_translations pull_translations push_translations \ + requirements shell swagger \ technical-docs test-requirements ubuntu-requirements upgrade-package upgrade # Careful with mktemp syntax: it has to work on Mac and Ubuntu, which have differences. @@ -55,8 +56,17 @@ endif push_translations: ## push source strings to Transifex for translation i18n_tool transifex push -pull_translations: ## pull translations from Transifex +pull_xblock_translations: ## pull xblock translations via atlas + rm -rf conf/plugins-locale # Clean up existing atlas translations + rm -rf lms/static/i18n/xblock.v1 cms/static/i18n/xblock.v1 # Clean up existing xblock compiled translations + mkdir -p conf/plugins-locale/xblock.v1/ lms/static/js/xblock.v1-i18n cms/static/js + python manage.py lms pull_xblock_translations --verbose $(ATLAS_OPTIONS) + python manage.py lms compile_xblock_translations + cp -r lms/static/js/xblock.v1-i18n cms/static/js + +pull_translations: ## pull translations from Transifex git clean -fdX conf/locale +ifeq ($(OPENEDX_ATLAS_PULL),) i18n_tool transifex pull i18n_tool extract i18n_tool dummy @@ -64,6 +74,12 @@ pull_translations: ## pull translations from Transifex git clean -fdX conf/locale/rtl git clean -fdX conf/locale/eo i18n_tool validate --verbose +else + make pull_xblock_translations + find conf/locale -mindepth 1 -maxdepth 1 -type d -exec rm -r {} \; + atlas pull $(ATLAS_OPTIONS) translations/edx-platform/conf/locale:conf/locale + i18n_tool generate +endif paver i18n_compilejs diff --git a/cms/djangoapps/cms_user_tasks/signals.py b/cms/djangoapps/cms_user_tasks/signals.py index 8011f2be1c55..b4f86807fd68 100644 --- a/cms/djangoapps/cms_user_tasks/signals.py +++ b/cms/djangoapps/cms_user_tasks/signals.py @@ -2,6 +2,7 @@ Receivers of signals sent from django-user-tasks """ import logging +import re from urllib.parse import urljoin from django.dispatch import receiver @@ -15,6 +16,7 @@ from .tasks import send_task_complete_email LOGGER = logging.getLogger(__name__) +LIBRARY_CONTENT_TASK_NAME_TEMPLATE = 'updating .*type@library_content.* from library' @receiver(user_task_stopped, dispatch_uid="cms_user_task_stopped") @@ -33,6 +35,22 @@ def user_task_stopped_handler(sender, **kwargs): # pylint: disable=unused-argum Returns: None """ + + def is_library_content_update(task_name: str) -> bool: + """ + Decides whether to suppress an end-of-task email on the basis that the just-ended task was a library content + XBlock update operation, and that emails following such operations amount to spam + Arguments: + task_name: The name of the just-ended task. By convention, if this was a library content XBlock update + task, then the task name follows the pattern prescribed in LibrarySyncChildrenTask + (content_libraries under openedx) 'Updating {key} from library'. Moreover, the block type + in the task name is always of type 'library_content' for such operations + Returns: + True if the end-of-task email should be suppressed + """ + p = re.compile(LIBRARY_CONTENT_TASK_NAME_TEMPLATE) + return p.match(task_name) + def get_olx_validation_from_artifact(): """ Get olx validation error if available for current task. @@ -47,9 +65,17 @@ def get_olx_validation_from_artifact(): return olx_artifact.text status = kwargs['status'] + # Only send email when the entire task is complete, should only send when # a chain / chord / etc completes, not on sub-tasks. if status.parent is None: + task_name = status.name.lower() + + # Also suppress emails on library content XBlock updates (too much like spam) + if is_library_content_update(task_name): + LOGGER.info(f"Suppressing end-of-task email on task {task_name}") + return + # `name` and `status` are not unique, first is our best guess artifact = UserTaskArtifact.objects.filter(status=status, name="BASE_URL").first() @@ -61,7 +87,6 @@ def get_olx_validation_from_artifact(): ) user_email = status.user.email - task_name = status.name.lower() olx_validation_text = get_olx_validation_from_artifact() task_args = [task_name, str(status.state_text), user_email, detail_url, olx_validation_text] try: diff --git a/cms/djangoapps/cms_user_tasks/tests.py b/cms/djangoapps/cms_user_tasks/tests.py index f50e5002feff..feeac4db0986 100644 --- a/cms/djangoapps/cms_user_tasks/tests.py +++ b/cms/djangoapps/cms_user_tasks/tests.py @@ -208,6 +208,19 @@ def test_email_sent_with_site(self): self.assert_msg_subject(msg) self.assert_msg_body_fragments(msg, body_fragments) + def test_email_not_sent_with_libary_content_update(self): + """ + Check the signal receiver and email sending. + """ + UserTaskArtifact.objects.create( + status=self.status, name='BASE_URL', url='https://test.edx.org/' + ) + end_of_task_status = self.status + end_of_task_status.name = "updating block-v1:course+type@library_content+block@uuid from library" + user_task_stopped.send(sender=UserTaskStatus, status=end_of_task_status) + + self.assertEqual(len(mail.outbox), 0) + def test_email_sent_with_olx_validations_with_config_enabled(self): """ Tests that email is sent with olx validation errors. diff --git a/cms/djangoapps/contentstore/asset_storage_handlers.py b/cms/djangoapps/contentstore/asset_storage_handlers.py index 1e4dbd61240b..281258e03a8d 100644 --- a/cms/djangoapps/contentstore/asset_storage_handlers.py +++ b/cms/djangoapps/contentstore/asset_storage_handlers.py @@ -126,28 +126,41 @@ def _get_asset_usage_path(course_key, assets): asset_key_string = str(asset_key) static_path = StaticContent.get_static_path_from_location(asset_key) is_video_block = getattr(block, 'category', '') == 'video' - if is_video_block: - handout = getattr(block, 'handout', '') - if handout and asset_key_string in handout: - unit = block.get_parent() - subsection = unit.get_parent() - subsection_display_name = getattr(subsection, 'display_name', '') - unit_display_name = getattr(unit, 'display_name', '') - xblock_display_name = getattr(block, 'display_name', '') - current_locations = usage_locations[asset_key_string] - new_location = f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}' - usage_locations[asset_key_string] = [*current_locations, new_location] - else: - data = getattr(block, 'data', '') - if static_path in data or asset_key_string in data: - unit = block.get_parent() - subsection = unit.get_parent() - subsection_display_name = getattr(subsection, 'display_name', '') - unit_display_name = getattr(unit, 'display_name', '') - xblock_display_name = getattr(block, 'display_name', '') - current_locations = usage_locations[asset_key_string] - new_location = f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}' - usage_locations[asset_key_string] = [*current_locations, new_location] + try: + if is_video_block: + handout = getattr(block, 'handout', '') + if handout and asset_key_string in handout: + usage_dict = {'display_location': '', 'url': ''} + xblock_display_name = getattr(block, 'display_name', '') + xblock_location = str(block.location) + unit = block.get_parent() + unit_location = str(block.parent) + unit_display_name = getattr(unit, 'display_name', '') + subsection = unit.get_parent() + subsection_display_name = getattr(subsection, 'display_name', '') + current_locations = usage_locations[asset_key_string] + usage_dict['display_location'] = (f'{subsection_display_name} - ' + f'{unit_display_name} / {xblock_display_name}') + usage_dict['url'] = f'/container/{unit_location}#{xblock_location}' + usage_locations[asset_key_string] = [*current_locations, usage_dict] + else: + data = getattr(block, 'data', '') + if static_path in data or asset_key_string in data: + usage_dict = {'display_location': '', 'url': ''} + xblock_display_name = getattr(block, 'display_name', '') + xblock_location = str(block.location) + unit = block.get_parent() + unit_location = str(block.parent) + unit_display_name = getattr(unit, 'display_name', '') + subsection = unit.get_parent() + subsection_display_name = getattr(subsection, 'display_name', '') + current_locations = usage_locations[asset_key_string] + usage_dict['display_location'] = (f'{subsection_display_name} - ' + f'{unit_display_name} / {xblock_display_name}') + usage_dict['url'] = f'/container/{unit_location}#{xblock_location}' + usage_locations[asset_key_string] = [*current_locations, usage_dict] + except AttributeError: + continue return usage_locations @@ -435,11 +448,15 @@ def _get_assets_in_json_format(assets, course_key, assets_usage_locations_map): for asset in assets: asset_key = asset['asset_key'] asset_key_string = str(asset_key) - usage_locations = getattr(assets_usage_locations_map, 'asset_key_string', []) thumbnail_asset_key = _get_thumbnail_asset_key(asset, course_key) asset_is_locked = asset.get('locked', False) asset_file_size = asset.get('length', None) + try: + usage_locations = assets_usage_locations_map[asset_key_string] + except KeyError: + usage_locations = [] + asset_in_json = get_asset_json( asset['displayname'], asset['contentType'], diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 3a9d5d92de42..8f59998e716c 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -19,6 +19,7 @@ from django.http import HttpResponseBadRequest from django.utils.translation import gettext as _ +from cms.djangoapps.contentstore.utils import track_course_update_event from openedx.core.lib.xblock_utils import get_course_update_items from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order @@ -85,6 +86,8 @@ def update_course_updates(location, update, passed_id=None, user=None): # update db record save_course_update_items(location, course_updates, course_update_items, user) + # track course update event + track_course_update_event(location.course_key, user, course_update_dict) # remove status key if "status" in course_update_dict: del course_update_dict["status"] diff --git a/cms/djangoapps/contentstore/docs/decisions/0003-hybrid-approach-for-public-apis.rst b/cms/djangoapps/contentstore/docs/decisions/0003-hybrid-approach-for-public-apis.rst index 57f3080c3b45..4efb932fb77f 100644 --- a/cms/djangoapps/contentstore/docs/decisions/0003-hybrid-approach-for-public-apis.rst +++ b/cms/djangoapps/contentstore/docs/decisions/0003-hybrid-approach-for-public-apis.rst @@ -1,6 +1,18 @@ 0003: Hybrid approach for public course authoring APIs ====================================================== +Status +------ + +Rejected + +Reason for rejection +-------------------- + +The objectives for public authoring APIs changed from the time this decision was made: +We are now limiting our offering to a set of experimental APIs with which to flesh our what a supported set of APIs might become. As such, the authoring APIs we are now implementing +are just a public set of wrappers around existing functionality, and are not fit for production course authoring. The responsibility for avoiding conflicts and resolving them, if they occur, is on the user. + Context ------- diff --git a/cms/djangoapps/contentstore/docs/decisions/0004-service-layer-for-contentstore-views.rst b/cms/djangoapps/contentstore/docs/decisions/0004-service-layer-for-contentstore-views.rst new file mode 100644 index 000000000000..73153940b26f --- /dev/null +++ b/cms/djangoapps/contentstore/docs/decisions/0004-service-layer-for-contentstore-views.rst @@ -0,0 +1,153 @@ +ADR 0004: Service Layer for Contentstore Views +============================================================= + +Status +------ +Accepted + +Context +------- +- The recent introduction of the public authoring API, which shares business logic with existing APIs for Micro-Frontends (MFEs), has led to redundant API implementations across various folders. +- Previously, business logic was embedded within lengthy view files, hindering reusability. +- To enhance maintainability and development efficiency, it's architecturally prudent to separate business logic from view-related code. + +Decision +-------- +- View files within ``cms/djangoapps/contentstore`` will exclusively handle API-layer operations. These responsibilities include, but are not limited to: + - Endpoint definitions + - Authorization processes + - Data validation + - Serialization tasks +- Business logic will be extracted and relocated as a distinct service layer to a folder called `edx-platform/cms/djangoapps/contentstore/core`, accountable for: + - Interactions with the modulestore + - All Create, Read, Update, Delete (CRUD) operations + - Data mapping and transformation + - Query-related logic + - Business domain-specific logic + - Functions not directly associated with API-layer tasks +- Given naming conflicts (e.g., with "Xblock Services"), we should generally avoid the term "service" where it could lead to confusion. + +Consequences +------------ +- Future view methods should confine business logic to the service layer (the `/core` folder). This ADR mandates the extraction of business logic from view files into the `/core` folder. There are no specific rules to how things in this folder should be named for now. + +Example +------- + +The following example shows a refactoring to this service layer pattern. + +Before refactoring, the view method implements some view-related logic like +authorization via `if not has_studio_read_access: ...` and serialization, +but also business logic: instantiating modulestore, fetching videos from it, +and then transforming the data to generate a new data structure `usage_locations`. + +After refactoring, the view method only implements logic related to the view / API layer, +and the business logic is extracted to a service file called `videos_provider.py` outside +the `views` folder. Now the videos provider is responsible for fetching and transforming +the data, while the view is responsible for authorization and serialization. + +Note that the file name `videos_provider.py` is a made-up example and is not a recommendation, since +we haven't determined any naming conventions at the time of writing this ADR +`(Discuss forum thread) `_. + + +**Before:**:: + + # cms/djangoapps/contentstore/views/videos.py + + @view_auth_classes(is_authenticated=True) + class VideoUsageView(DeveloperErrorViewMixin, APIView): + @verify_course_exists() + def get(self, request: Request, course_id: str, edx_video_id: str): + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + store = modulestore() + usage_locations = [] + videos = store.get_items( + course_key, + qualifiers={ + 'category': 'video' + }, + ) + for video in videos: + video_id = getattr(video, 'edx_video_id', '') + if video_id == edx_video_id: + unit = video.get_parent() + subsection = unit.get_parent() + subsection_display_name = getattr(subsection, 'display_name', '') + unit_display_name = getattr(unit, 'display_name', '') + xblock_display_name = getattr(video, 'display_name', '') + usage_locations.append(f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}') + + formatted_usage_locations = {'usage_locations': usage_locations} + serializer = VideoUsageSerializer(formatted_usage_locations) + return Response(serializer.data) + +**After:**:: + + # cms/djangoapps/contentstore/views/videos.py + + @view_auth_classes(is_authenticated=True) + class VideoUsageView(DeveloperErrorViewMixin, APIView): + @verify_course_exists() + def get(self, request: Request, course_id: str, edx_video_id: str): + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + usage_locations = get_video_usage_path(course_key, edx_video_id) + serializer = VideoUsageSerializer(usage_locations) + return Response(serializer.data) + + # cms/djangoapps/contentstore/core/videos_provider.py + + def get_video_usage_path(course_key, edx_video_id): + """ + API for fetching the locations a specific video is used in a course. + Returns a list of paths to a video. + """ + store = modulestore() + usage_locations = [] + videos = store.get_items( + course_key, + qualifiers={ + 'category': 'video' + }, + ) + for video in videos: + video_id = getattr(video, 'edx_video_id', '') + if video_id == edx_video_id: + unit = video.get_parent() + subsection = unit.get_parent() + subsection_display_name = getattr(subsection, 'display_name', '') + unit_display_name = getattr(unit, 'display_name', '') + xblock_display_name = getattr(video, 'display_name', '') + usage_locations.append(f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}') + return {'usage_locations': usage_locations} + +Rejected Alternatives +--------------------- +Contentstore may be becoming too big and may warrant being split up into multiple djangoapps. However, that would be a much larger and different refactoring effort and is not considered necessary at this point. By implementing this ADR we are not preventing this from happening later, so we decided to follow the patterns described in this ADR for now. + +Community Feedback +------------------ +The following feedback about this ADR is considered out of scope here, but consists of relevant recommendations from the community. (`Source `_) + +1. Code in `contentstore/api` should be for Python API that can be consumed by other edx-platform apps, as per `OEP-49 `_. +2. "One recommendation I’d add is the use of a `data.py module `_ for immutable domain-layer attrs classes (dataclasses are good too, they just weren’t available when that OEP was written) which can be passed around in place of models or entire xblocks. (`Example `_) If there are data classes that you’d rather not expose in the public API, maybe you could have two data modules: + - cms/djangoapps/contentstore/data.py – domain objects exposed by the public python API + - cms/djangoapps/contentstore/core/data.py – domain objects for internal business logic" +3. "Another recommendation is to be wary of deep nesting and long names. There’s a non-trivial cognitive load that is added when we have modules paths like openedx/core/djangoapps/content/foo/bar/bar_providers.py instead of, e.g., common/core/foo/bar.py. I know you’re working within the existing framework of edx-platform’s folder structure, so there’s only so much you can do here" +4. "once the refactoring is done, if we like how the end result looks and think it’d generalize well to other apps, I suggest that we update OEP-49 with the structure." + + +Notes +----- +- Identifying a good way to structure file and folder naming and architecture around this is + discussed in `this forum post `_. +- The terms "service" / "service layer" are distinct from "Xblock Services" and should not be conflated with them. +- For a deeper understanding of service layer concepts, refer to `Cosmic Python, Chapter 4: Service Layer `_. diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index c2cc876f19da..9fd09193b59f 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -17,6 +17,7 @@ from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore from xmodule.exceptions import NotFoundError +from xmodule.library_content_block import LibraryContentBlock from xmodule.modulestore.django import modulestore from xmodule.xml_block import XmlMixin @@ -302,6 +303,16 @@ def _import_xml_node_to_parent( # and VAL will thus make the transcript available. child_nodes = [] + + if issubclass(xblock_class, XmlMixin): + # Hack: XBlocks that use "XmlMixin" have their own XML parsing behavior, and in particular if they encounter + # an XML node that has no children and has only a "url_name" attribute, they'll try to load the XML data + # from an XML file in runtime.resources_fs. But that file doesn't exist here. So we set at least one + # additional attribute here to make sure that url_name is not the only attribute; otherwise in some cases, + # XmlMixin.parse_xml will try to load an XML file that doesn't exist, giving an error. The name and value + # of this attribute don't matter and should be ignored. + node.attrib["x-is-pointer-node"] = "no" + if not xblock_class.has_children: # No children to worry about. The XML may contain child nodes, but they're not XBlocks. temp_xblock = xblock_class.parse_xml(node, runtime, keys, id_generator) @@ -314,14 +325,6 @@ def _import_xml_node_to_parent( # serialization of a child block, in order. For blocks that don't support children, their XML content/nodes # could be anything (e.g. HTML, capa) node_without_children = etree.Element(node.tag, **node.attrib) - if issubclass(xblock_class, XmlMixin): - # Hack: XBlocks that use "XmlMixin" have their own XML parsing behavior, and in particular if they encounter - # an XML node that has no children and has only a "url_name" attribute, they'll try to load the XML data - # from an XML file in runtime.resources_fs. But that file doesn't exist here. So we set at least one - # additional attribute here to make sure that url_name is not the only attribute; otherwise in some cases, - # XmlMixin.parse_xml will try to load an XML file that doesn't exist, giving an error. The name and value - # of this attribute don't matter and should be ignored. - node_without_children.attrib["x-is-pointer-node"] = "no" temp_xblock = xblock_class.parse_xml(node_without_children, runtime, keys, id_generator) child_nodes = list(node) if xblock_class.has_children and temp_xblock.children: @@ -334,8 +337,14 @@ def _import_xml_node_to_parent( new_xblock = store.update_item(temp_xblock, user_id, allow_not_found=True) parent_xblock.children.append(new_xblock.location) store.update_item(parent_xblock, user_id) - for child_node in child_nodes: - _import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id) + if isinstance(new_xblock, LibraryContentBlock): + # Special case handling for library content. If we need this for other blocks in the future, it can be made into + # an API, and we'd call new_block.studio_post_paste() instead of this code. + # In this case, we want to pull the children from the library and let library_tools assign their IDs. + new_xblock.sync_from_library(upgrade_to_latest=False) + else: + for child_node in child_nodes: + _import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id) return new_xblock diff --git a/cms/djangoapps/contentstore/management/commands/reindex_course.py b/cms/djangoapps/contentstore/management/commands/reindex_course.py index 2da8d352f197..74317169957a 100644 --- a/cms/djangoapps/contentstore/management/commands/reindex_course.py +++ b/cms/djangoapps/contentstore/management/commands/reindex_course.py @@ -3,6 +3,8 @@ import logging from textwrap import dedent +from time import time +from datetime import date from django.core.management import BaseCommand, CommandError from elasticsearch import exceptions @@ -24,7 +26,7 @@ class Command(BaseCommand): Examples: ./manage.py reindex_course ... - reindexes courses with provided keys - ./manage.py reindex_course --all - reindexes all available courses + ./manage.py reindex_course --all --warning - reindexes all available courses with quieter logging ./manage.py reindex_course --setup - reindexes all courses for devstack setup """ help = dedent(__doc__) @@ -37,9 +39,16 @@ def add_arguments(self, parser): parser.add_argument('--all', action='store_true', help='Reindex all courses') + parser.add_argument('--active', + action='store_true', + help='Reindex active courses only') parser.add_argument('--setup', action='store_true', help='Reindex all courses on developers stack setup') + parser.add_argument('--warning', + action='store_true', + help='Reduce logging to a WARNING level of output for progress tracking' + ) def _parse_course_key(self, raw_value): """ Parses course key from string """ @@ -60,14 +69,24 @@ def handle(self, *args, **options): """ course_ids = options['course_ids'] all_option = options['all'] + active_option = options['active'] setup_option = options['setup'] + readable_option = options['warning'] index_all_courses_option = all_option or setup_option - if (not len(course_ids) and not index_all_courses_option) or (len(course_ids) and index_all_courses_option): # lint-amnesty, pylint: disable=len-as-condition - raise CommandError("reindex_course requires one or more s OR the --all or --setup flags.") + if ((not course_ids and not (index_all_courses_option or active_option)) or + (course_ids and (index_all_courses_option or active_option))): + raise CommandError(( + "reindex_course requires one or more s" + " OR the --all, --active or --setup flags." + )) store = modulestore() + if readable_option: + logging.disable(level=logging.INFO) + logging.warning('Reducing logging to WARNING level for easier progress tracking') + if index_all_courses_option: index_names = (CoursewareSearchIndexer.INDEX_NAME, CourseAboutSearchIndexer.INDEX_NAME) if setup_option: @@ -94,12 +113,38 @@ def handle(self, *args, **options): course_keys = [course.id for course in modulestore().get_courses()] else: return + elif active_option: + # in case of --active, we get the list of course keys from all courses + # that are stored in the modulestore and filter out the non-active + course_keys = [] + + today = date.today() + all_courses = modulestore().get_courses() + for course in all_courses: + # Omitting courses without a start date as well as + # couses that already ended (end date is in the past) + if not course.start or (course.end and course.end.date() < today): + continue + course_keys.append(course.id) + + logging.warning(f'Selected {len(course_keys)} active courses over a total of {len(all_courses)}.') + else: # in case course keys are provided as arguments course_keys = list(map(self._parse_course_key, course_ids)) + total = len(course_keys) + logging.warning(f'Reindexing {total} courses...') + reindexed = 0 + start = time() + for course_key in course_keys: try: CoursewareSearchIndexer.do_course_reindex(store, course_key) + reindexed += 1 + if reindexed % 10 == 0 or reindexed == total: + now = time() + t = now - start + logging.warning(f'{reindexed}/{total} reindexed in {t:.1f} seconds.') except Exception as exc: # lint-amnesty, pylint: disable=broad-except - logging.exception('Error indexing course %s due to the error: %s', course_key, exc) + logging.exception('Error indexing course %s due to the error: %s.', course_key, exc) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py index 57534f30c4a0..13d33c48f92a 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py @@ -10,6 +10,7 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory # lint-amnesty, pylint: disable=wrong-import-order +from datetime import datetime, timedelta @ddt.ddt @@ -26,11 +27,18 @@ def setUp(self): org="test", library="lib2", display_name="run2", default_store=ModuleStoreEnum.Type.split ) + yesterday = datetime.min.today() - timedelta(days=1) + self.first_course = CourseFactory.create( - org="test", course="course1", display_name="run1" + org="test", course="course1", display_name="run1", start=yesterday, end=None ) + self.second_course = CourseFactory.create( - org="test", course="course2", display_name="run1" + org="test", course="course2", display_name="run1", start=yesterday, end=yesterday + ) + + self.third_course = CourseFactory.create( + org="test", course="course3", display_name="run1", start=None, end=None ) REINDEX_PATH_LOCATION = ( @@ -103,7 +111,7 @@ def test_given_all_key_prompts_and_reindexes_all_courses(self): call_command('reindex_course', all=True) patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') - expected_calls = self._build_calls(self.first_course, self.second_course) + expected_calls = self._build_calls(self.first_course, self.second_course, self.third_course) self.assertCountEqual(patched_index.mock_calls, expected_calls) def test_given_all_key_prompts_and_reindexes_all_courses_cancelled(self): @@ -116,3 +124,15 @@ def test_given_all_key_prompts_and_reindexes_all_courses_cancelled(self): patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') patched_index.assert_not_called() + + def test_given_active_key_prompt(self): + """ + Test that reindexes all active courses when --active key is given + Active courses have a start date but no end date, or the end date is in the future. + """ + with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ + mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): + call_command('reindex_course', active=True) + + expected_calls = self._build_calls(self.first_course) + self.assertCountEqual(patched_index.mock_calls, expected_calls) diff --git a/cms/djangoapps/contentstore/rest_api/serializers/common.py b/cms/djangoapps/contentstore/rest_api/serializers/common.py index 362504ccb0c1..824054330fc2 100644 --- a/cms/djangoapps/contentstore/rest_api/serializers/common.py +++ b/cms/djangoapps/contentstore/rest_api/serializers/common.py @@ -50,3 +50,23 @@ def to_internal_value(self, data): ) return ret + + +class ProctoringErrorModelSerializer(serializers.Serializer): + """ + Serializer for proctoring error model item. + """ + deprecated = serializers.BooleanField() + display_name = serializers.CharField() + help = serializers.CharField() + hide_on_enabled_publisher = serializers.BooleanField() + value = serializers.CharField() + + +class ProctoringErrorListSerializer(serializers.Serializer): + """ + Serializer for proctoring error list. + """ + key = serializers.CharField() + message = serializers.CharField() + model = ProctoringErrorModelSerializer() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index ac1f2cd1fb54..7e99a729abb3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -4,8 +4,9 @@ from .course_details import CourseDetailsSerializer from .course_rerun import CourseRerunSerializer from .course_team import CourseTeamSerializer +from .course_index import CourseIndexSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer -from .home import CourseHomeSerializer +from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, @@ -17,5 +18,6 @@ CourseVideosSerializer, VideoUploadSerializer, VideoImageSerializer, - VideoUsageSerializer + VideoUsageSerializer, + VideoDownloadSerializer ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py new file mode 100644 index 000000000000..d423f7e9dab4 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_index.py @@ -0,0 +1,31 @@ +""" +API Serializers for course index +""" + +from rest_framework import serializers + +from cms.djangoapps.contentstore.rest_api.serializers.common import ProctoringErrorListSerializer + + +class InitialIndexStateSerializer(serializers.Serializer): + """Serializer for initial course index state""" + expanded_locators = serializers.ListSerializer(child=serializers.CharField()) + locator_to_show = serializers.CharField() + + +class CourseIndexSerializer(serializers.Serializer): + """Serializer for course index""" + course_release_date = serializers.CharField() + course_structure = serializers.DictField() + deprecated_blocks_info = serializers.DictField() + discussions_incontext_feedback_url = serializers.CharField() + discussions_incontext_learnmore_url = serializers.CharField() + initial_state = InitialIndexStateSerializer() + initial_user_clipboard = serializers.DictField() + language_code = serializers.CharField() + lms_link = serializers.CharField() + mfe_proctored_exam_settings_url = serializers.CharField() + notification_dismiss_url = serializers.CharField() + proctoring_errors = ProctoringErrorListSerializer(many=True) + reindex_link = serializers.CharField() + rerun_notification_id = serializers.IntegerField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 12816a8cbd1d..80296b9a766c 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -31,6 +31,16 @@ class LibraryViewSerializer(serializers.Serializer): can_edit = serializers.BooleanField() +class CourseTabSerializer(serializers.Serializer): + archived_courses = CourseCommonSerializer(required=False, many=True) + courses = CourseCommonSerializer(required=False, many=True) + in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True) + + +class LibraryTabSerializer(serializers.Serializer): + libraries = LibraryViewSerializer(many=True, required=False, allow_null=True) + + class CourseHomeSerializer(serializers.Serializer): """Serializer for course home""" allow_course_reruns = serializers.BooleanField() diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py index df5b77f72f02..8398bd8ad626 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/proctoring.py @@ -4,6 +4,7 @@ from rest_framework import serializers +from cms.djangoapps.contentstore.rest_api.serializers.common import ProctoringErrorListSerializer from xmodule.course_block import get_available_providers @@ -31,26 +32,6 @@ class ProctoredExamConfigurationSerializer(serializers.Serializer): course_start_date = serializers.DateTimeField() -class ProctoringErrorModelSerializer(serializers.Serializer): - """ - Serializer for proctoring error model item. - """ - deprecated = serializers.BooleanField() - display_name = serializers.CharField() - help = serializers.CharField() - hide_on_enabled_publisher = serializers.BooleanField() - value = serializers.CharField() - - -class ProctoringErrorListSerializer(serializers.Serializer): - """ - Serializer for proctoring error list. - """ - key = serializers.CharField() - message = serializers.CharField() - model = ProctoringErrorModelSerializer() - - class ProctoringErrorsSerializer(serializers.Serializer): """ Serializer for proctoring errors with url to proctored exam settings. diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py index 04f508eedfd7..bd85fdb77220 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -56,9 +56,6 @@ class VideoModelSerializer(serializers.Serializer): transcripts = serializers.ListField( child=serializers.CharField() ) - usage_locations = serializers.ListField( - child=serializers.CharField() - ) class VideoActiveTranscriptPreferencesSerializer(serializers.Serializer): @@ -89,6 +86,7 @@ class CourseVideosSerializer(serializers.Serializer): video_upload_max_file_size = serializers.CharField() video_image_settings = VideoImageSettingsSerializer(required=True, allow_null=False) is_video_transcript_enabled = serializers.BooleanField() + is_ai_translations_enabled = serializers.BooleanField() active_transcript_preferences = VideoActiveTranscriptPreferencesSerializer(required=False, allow_null=True) transcript_credentials = serializers.DictField( child=serializers.BooleanField() @@ -98,7 +96,6 @@ class CourseVideosSerializer(serializers.Serializer): child=serializers.CharField() ) ) - # transcript_available_languages = serializers.BooleanField(required=False, allow_null=True) video_transcript_settings = VideoTranscriptSettingsSerializer() pagination_context = serializers.DictField( child=serializers.CharField(), @@ -110,7 +107,16 @@ class CourseVideosSerializer(serializers.Serializer): class VideoUsageSerializer(serializers.Serializer): """Serializer for video usage""" usage_locations = serializers.ListField( - child=serializers.CharField() + child=serializers.DictField() + ) + + +class VideoDownloadSerializer(serializers.Serializer): + """Serializer for video downloads""" + files = serializers.ListField( + child=serializers.DictField( + child=serializers.CharField() + ) ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index ad5765a67396..1af7cf46a675 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -7,15 +7,19 @@ from .views import ( CourseDetailsView, CourseTeamView, + CourseIndexView, CourseGradingView, CourseRerunView, CourseSettingsView, CourseVideosView, HomePageView, + HomePageCoursesView, + HomePageLibrariesView, ProctoredExamSettingsView, ProctoringErrorsView, HelpUrlsView, - VideoUsageView + VideoUsageView, + VideoDownloadView ) app_name = 'v1' @@ -28,6 +32,14 @@ HomePageView.as_view(), name="home" ), + path( + 'home/courses', + HomePageCoursesView.as_view(), + name="courses"), + path( + 'home/libraries', + HomePageLibrariesView.as_view(), + name="libraries"), re_path( fr'^videos/{COURSE_ID_PATTERN}$', CourseVideosView.as_view(), @@ -38,6 +50,11 @@ VideoUsageView.as_view(), name="video_usage" ), + re_path( + fr'^videos/{COURSE_ID_PATTERN}/download$', + VideoDownloadView.as_view(), + name="video_usage" + ), re_path( fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$', ProctoredExamSettingsView.as_view(), @@ -53,6 +70,11 @@ CourseSettingsView.as_view(), name="course_settings" ), + re_path( + fr'^course_index/{COURSE_ID_PATTERN}$', + CourseIndexView.as_view(), + name="course_index" + ), re_path( fr'^course_details/{COURSE_ID_PATTERN}$', CourseDetailsView.as_view(), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 780f0059aaed..57d68ebd081f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -2,14 +2,16 @@ Views for v1 contentstore API. """ from .course_details import CourseDetailsView +from .course_index import CourseIndexView from .course_team import CourseTeamView from .course_rerun import CourseRerunView from .grading import CourseGradingView from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView -from .home import HomePageView +from .home import HomePageView, HomePageCoursesView, HomePageLibrariesView from .settings import CourseSettingsView from .videos import ( CourseVideosView, VideoUsageView, + VideoDownloadView ) from .help_urls import HelpUrlsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py new file mode 100644 index 000000000000..1ffac5ba6900 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py @@ -0,0 +1,98 @@ +"""API Views for course index""" + +import edx_api_doc_tools as apidocs +from django.conf import settings +from opaque_keys.edx.keys import CourseKey +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseIndexSerializer +from cms.djangoapps.contentstore.utils import get_course_index_context +from common.djangoapps.student.auth import has_studio_read_access +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes + + +@view_auth_classes(is_authenticated=True) +class CourseIndexView(DeveloperErrorViewMixin, APIView): + """View for Course Index""" + + @apidocs.schema( + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + apidocs.string_parameter( + "show", + apidocs.ParameterLocation.QUERY, + description="Query param to set initial state which fully expanded to see the item", + )], + responses={ + 200: CourseIndexSerializer, + 401: "The requester is not authenticated.", + 403: "The requester cannot access the specified course.", + 404: "The requested course does not exist.", + }, + ) + @verify_course_exists() + def get(self, request: Request, course_id: str): + """ + Get an object containing course index for outline. + + **Example Request** + + GET /api/contentstore/v1/course_index/{course_id}?show=block-v1:edx+101+y+type@course+block@course + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned. + + The HTTP 200 response contains a single dict that contains keys that + are the course's outline. + + **Example Response** + + ```json + { + "course_release_date": "Set Date", + "course_structure": {}, + "deprecated_blocks_info": { + "deprecated_enabled_block_types": [], + "blocks": [], + "advance_settings_url": "/settings/advanced/course-v1:edx+101+y76" + }, + "discussions_incontext_feedback_url": "", + "discussions_incontext_learnmore_url": "", + "initial_state": { + "expanded_locators": [ + "block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6", + "block-v1:edx+101+y76+type@sequential+block@8a85e287e30a47e98d8c1f37f74a6a9d" + ], + "locator_to_show": "block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6" + }, + "initial_user_clipboard": { + "content": null, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "language_code": "en", + "lms_link": "//localhost:18000/courses/course-v1:edx+101+y76/jump_to/block-v1:edx+101+y76", + "mfe_proctored_exam_settings_url": "", + "notification_dismiss_url": "/course_notifications/course-v1:edx+101+y76/2", + "proctoring_errors": [], + "reindex_link": "/course/course-v1:edx+101+y76/search_reindex", + "rerun_notification_id": 2 + } + ``` + """ + + course_key = CourseKey.from_string(course_id) + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + course_index_context = get_course_index_context(request, course_key) + course_index_context.update({ + "discussions_incontext_learnmore_url": settings.DISCUSSIONS_INCONTEXT_LEARNMORE_URL, + "discussions_incontext_feedback_url": settings.DISCUSSIONS_INCONTEXT_FEEDBACK_URL, + }) + + serializer = CourseIndexSerializer(course_index_context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index ea0724e8e2df..f8ee907d2e9f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -7,8 +7,8 @@ from rest_framework.views import APIView from openedx.core.lib.api.view_utils import view_auth_classes -from ....utils import get_home_context -from ..serializers import CourseHomeSerializer +from ....utils import get_home_context, get_course_context, get_library_context +from ..serializers import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer @view_auth_classes(is_authenticated=True) @@ -51,6 +51,80 @@ def get(self, request: Request): "allow_to_create_new_org": true, "allow_unicode_course_id": false, "allowed_organizations": [], + "archived_courses": [], + "can_create_organizations": true, + "course_creator_status": "granted", + "courses": [], + "in_process_course_actions": [], + "libraries": [], + "libraries_enabled": true, + "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", + "optimization_enabled": true, + "redirect_to_library_authoring_mfe": false, + "request_course_creator_url": "/request_course_creator", + "rerun_creator_status": true, + "show_new_library_button": true, + "split_studio_home": false, + "studio_name": "Studio", + "studio_short_name": "Studio", + "studio_request_email": "", + "tech_support_email": "technical@example.com", + "platform_name": "Your Platform Name Here" + "user_is_active": true, + } + ``` + """ + + home_context = get_home_context(request, True) + home_context.update({ + 'allow_to_create_new_org': settings.FEATURES.get('ENABLE_CREATOR_GROUP', True) and request.user.is_staff, + 'studio_name': settings.STUDIO_NAME, + 'studio_short_name': settings.STUDIO_SHORT_NAME, + 'studio_request_email': settings.FEATURES.get('STUDIO_REQUEST_EMAIL', ''), + 'tech_support_email': settings.TECH_SUPPORT_EMAIL, + 'platform_name': settings.PLATFORM_NAME, + 'user_is_active': request.user.is_active, + }) + serializer = CourseHomeSerializer(home_context) + return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class HomePageCoursesView(APIView): + """ + View for getting all courses and libraries available to the logged in user. + """ + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "org", + apidocs.ParameterLocation.QUERY, + description="Query param to filter by course org", + )], + responses={ + 200: CourseTabSerializer, + 401: "The requester is not authenticated.", + }, + ) + def get(self, request: Request): + """ + Get an object containing all courses. + + **Example Request** + + GET /api/contentstore/v1/home/courses + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned. + + The HTTP 200 response contains a single dict that contains keys that + are the course's home. + + **Example Response** + + ```json + { "archived_courses": [ { "course_key": "course-v1:edX+P315+2T2023", @@ -63,8 +137,6 @@ def get(self, request: Request): "url": "/course/course-v1:edX+P315+2T2023" }, ], - "can_create_organizations": true, - "course_creator_status": "granted", "courses": [ { "course_key": "course-v1:edX+E2E-101+course", @@ -78,6 +150,56 @@ def get(self, request: Request): }, ], "in_process_course_actions": [], + } + ``` + """ + + active_courses, archived_courses, in_process_course_actions = get_course_context(request) + courses_context = { + "courses": active_courses, + "archived_courses": archived_courses, + "in_process_course_actions": in_process_course_actions, + } + serializer = CourseTabSerializer(courses_context) + return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class HomePageLibrariesView(APIView): + """ + View for getting all courses and libraries available to the logged in user. + """ + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "org", + apidocs.ParameterLocation.QUERY, + description="Query param to filter by course org", + )], + responses={ + 200: LibraryTabSerializer, + 401: "The requester is not authenticated.", + }, + ) + def get(self, request: Request): + """ + Get an object containing all libraries on home page. + + **Example Request** + + GET /api/contentstore/v1/home/libraries + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned. + + The HTTP 200 response contains a single dict that contains keys that + are the course's home. + + **Example Response** + + ```json + { "libraries": [ { "display_name": "My First Library", @@ -87,34 +209,10 @@ def get(self, request: Request): "number": "CPSPR", "can_edit": true } - ], - "libraries_enabled": true, - "library_authoring_mfe_url": "//localhost:3001/course/course-v1:edX+P315+2T2023", - "optimization_enabled": true, - "redirect_to_library_authoring_mfe": false, - "request_course_creator_url": "/request_course_creator", - "rerun_creator_status": true, - "show_new_library_button": true, - "split_studio_home": false, - "studio_name": "Studio", - "studio_short_name": "Studio", - "studio_request_email": "", - "tech_support_email": "technical@example.com", - "platform_name": "Your Platform Name Here" - "user_is_active": true, - } + ], } ``` """ - home_context = get_home_context(request) - home_context.update({ - 'allow_to_create_new_org': settings.FEATURES.get('ENABLE_CREATOR_GROUP', True) and request.user.is_staff, - 'studio_name': settings.STUDIO_NAME, - 'studio_short_name': settings.STUDIO_SHORT_NAME, - 'studio_request_email': settings.FEATURES.get('STUDIO_REQUEST_EMAIL', ''), - 'tech_support_email': settings.TECH_SUPPORT_EMAIL, - 'platform_name': settings.PLATFORM_NAME, - 'user_is_active': request.user.is_active, - }) - serializer = CourseHomeSerializer(home_context) + library_context = get_library_context(request) + serializer = LibraryTabSerializer(library_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py new file mode 100644 index 000000000000..e65ae8429567 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_index.py @@ -0,0 +1,134 @@ +""" +Unit tests for course index outline. +""" +from django.test import RequestFactory +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.rest_api.v1.mixins import PermissionAccessMixin +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.utils import get_lms_link_for_item +from cms.djangoapps.contentstore.views.course import _course_outline_json +from common.djangoapps.student.tests.factories import UserFactory +from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES +from xmodule.modulestore.tests.factories import BlockFactory, check_mongo_calls + + +class CourseIndexViewTest(CourseTestCase, PermissionAccessMixin): + """ + Tests for CourseIndexView. + """ + + def setUp(self): + super().setUp() + with self.store.bulk_operations(self.course.id, emit_signals=False): + self.chapter = BlockFactory.create( + parent=self.course, display_name='Overview' + ) + self.section = BlockFactory.create( + parent=self.chapter, display_name='Welcome' + ) + self.unit = BlockFactory.create( + parent=self.section, display_name='New Unit' + ) + self.xblock = BlockFactory.create( + parent=self.unit, + category='problem', + display_name='Some problem' + ) + self.user = UserFactory() + self.factory = RequestFactory() + self.request = self.factory.get(f"/course/{self.course.id}") + self.request.user = self.user + self.reload_course() + self.url = reverse( + "cms.djangoapps.contentstore:v1:course_index", + kwargs={"course_id": self.course.id}, + ) + + def test_course_index_response(self): + """Check successful response content""" + response = self.client.get(self.url) + expected_response = { + "course_release_date": "Set Date", + "course_structure": _course_outline_json(self.request, self.course), + "deprecated_blocks_info": { + "deprecated_enabled_block_types": [], + "blocks": [], + "advance_settings_url": f"/settings/advanced/{self.course.id}" + }, + "discussions_incontext_feedback_url": "", + "discussions_incontext_learnmore_url": "", + "initial_state": None, + "initial_user_clipboard": { + "content": None, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "language_code": "en", + "lms_link": get_lms_link_for_item(self.course.location), + "mfe_proctored_exam_settings_url": "", + "notification_dismiss_url": None, + "proctoring_errors": [], + "reindex_link": f"/course/{self.course.id}/search_reindex", + "rerun_notification_id": None + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + + def test_course_index_response_with_show_locators(self): + """Check successful response content with show query param""" + response = self.client.get(self.url, {"show": str(self.unit.location)}) + expected_response = { + "course_release_date": "Set Date", + "course_structure": _course_outline_json(self.request, self.course), + "deprecated_blocks_info": { + "deprecated_enabled_block_types": [], + "blocks": [], + "advance_settings_url": f"/settings/advanced/{self.course.id}" + }, + "discussions_incontext_feedback_url": "", + "discussions_incontext_learnmore_url": "", + "initial_state": { + "expanded_locators": [ + str(self.unit.location), + str(self.xblock.location), + ], + "locator_to_show": str(self.unit.location), + }, + "initial_user_clipboard": { + "content": None, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "language_code": "en", + "lms_link": get_lms_link_for_item(self.course.location), + "mfe_proctored_exam_settings_url": "", + "notification_dismiss_url": None, + "proctoring_errors": [], + "reindex_link": f"/course/{self.course.id}/search_reindex", + "rerun_notification_id": None + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + + def test_course_index_response_with_invalid_course(self): + """Check error response for invalid course id""" + response = self.client.get(self.url + "1") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.data, { + "developer_message": f"Unknown course {self.course.id}1", + "error_code": "course_does_not_exist" + }) + + def test_number_of_calls_to_db(self): + """ + Test to check number of queries made to mysql and mongo + """ + with self.assertNumQueries(29, table_ignorelist=WAFFLE_TABLES): + with check_mongo_calls(3): + self.client.get(self.url) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 99f1b450cc0d..5279af0b1297 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -11,6 +11,7 @@ from rest_framework import status from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase from cms.djangoapps.contentstore.views.course import ENABLE_GLOBAL_STAFF_OPTIMIZATION from cms.djangoapps.contentstore.toggles import ENABLE_TAGGING_TAXONOMY_LIST_PAGE from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory @@ -20,17 +21,16 @@ @ddt.ddt class HomePageViewTest(CourseTestCase): """ - Tests for HomePageView. + Tests for HomePageCoursesView. """ def setUp(self): super().setUp() self.url = reverse("cms.djangoapps.contentstore:v1:home") - def test_home_page_response(self): + def test_home_page_courses_response(self): """Check successful response content""" response = self.client.get(self.url) - course_id = str(self.course.id) expected_response = { "allow_course_reruns": True, @@ -40,16 +40,7 @@ def test_home_page_response(self): "archived_courses": [], "can_create_organizations": True, "course_creator_status": "granted", - "courses": [{ - "course_key": course_id, - "display_name": self.course.display_name, - "lms_link": f'//{settings.LMS_BASE}/courses/{course_id}/jump_to/{self.course.location}', - "number": self.course.number, - "org": self.course.org, - "rerun_link": f'/course_rerun/{course_id}', - "run": self.course.id.run, - "url": f'/course/{course_id}', - }], + "courses": [], "in_process_course_actions": [], "libraries": [], "libraries_enabled": True, @@ -73,6 +64,50 @@ def test_home_page_response(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(expected_response, response.data) + @override_waffle_flag(ENABLE_TAGGING_TAXONOMY_LIST_PAGE, True) + def test_taxonomy_list_link(self): + response = self.client.get(self.url) + self.assertTrue(response.data['taxonomies_enabled']) + self.assertEqual( + response.data['taxonomy_list_mfe_url'], + f'{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/taxonomies' + ) + + +@ddt.ddt +class HomePageCoursesViewTest(CourseTestCase): + """ + Tests for HomePageView. + """ + + def setUp(self): + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v1:courses") + + def test_home_page_response(self): + """Check successful response content""" + response = self.client.get(self.url) + course_id = str(self.course.id) + + expected_response = { + "archived_courses": [], + "courses": [{ + "course_key": course_id, + "display_name": self.course.display_name, + "lms_link": f'//{settings.LMS_BASE}/courses/{course_id}/jump_to/{self.course.location}', + "number": self.course.number, + "org": self.course.org, + "rerun_link": f'/course_rerun/{course_id}', + "run": self.course.id.run, + "url": f'/course/{course_id}', + }], + "in_process_course_actions": [], + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + print(response.data) + self.assertDictEqual(expected_response, response.data) + @override_waffle_switch(ENABLE_GLOBAL_STAFF_OPTIMIZATION, True) def test_org_query_if_passed(self): """Test home page when org filter passed as a query param""" @@ -94,11 +129,32 @@ def test_org_query_if_empty(self): self.assertEqual(len(response.data['courses']), 0) self.assertEqual(response.status_code, status.HTTP_200_OK) - @override_waffle_flag(ENABLE_TAGGING_TAXONOMY_LIST_PAGE, True) - def test_taxonomy_list_link(self): + +@ddt.ddt +class HomePageLibrariesViewTest(LibraryTestCase): + """ + Tests for HomePageLibrariesView. + """ + + def setUp(self): + super().setUp() + self.url = reverse("cms.djangoapps.contentstore:v1:libraries") + + def test_home_page_libraries_response(self): + """Check successful response content""" response = self.client.get(self.url) - self.assertTrue(response.data['taxonomies_enabled']) - self.assertEqual( - response.data['taxonomy_list_mfe_url'], - f'{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/taxonomy-list' - ) + + expected_response = { + "libraries": [{ + 'display_name': 'Test Library', + 'library_key': 'library-v1:org+lib', + 'url': '/library/library-v1:org+lib', + 'org': 'org', + 'number': 'lib', + 'can_edit': True + }], + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + print(response.data) + self.assertDictEqual(expected_response, response.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py index d5d277c498dc..993072e0717f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py @@ -56,6 +56,7 @@ def test_course_videos_response(self): "supported_file_formats": settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS }, "is_video_transcript_enabled": False, + "is_ai_translations_enabled": False, "active_transcript_preferences": None, "transcript_credentials": None, "transcript_available_languages": get_all_transcript_languages(), @@ -126,3 +127,10 @@ def test_VideoTranscriptEnabledFlag_enabled(self): ) self.assertIn("transcript_credentials_handler_url", transcript_settings) self.assertEqual(expected_credentials_handler, transcript_settings["transcript_credentials_handler_url"]) + with patch( + 'openedx.core.djangoapps.video_config.toggles.XPERT_TRANSLATIONS_UI.is_enabled' + ) as xpertTranslationfeature: + xpertTranslationfeature.return_value = True + response = self.client.get(self.url) + self.assertIn("is_ai_translations_enabled", response.data) + self.assertTrue(response.data["is_ai_translations_enabled"]) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index c5e7ae1bb263..c41deec0e5da 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -14,11 +14,13 @@ from ....utils import get_course_videos_context from cms.djangoapps.contentstore.video_storage_handlers import ( - get_video_usage_path + get_video_usage_path, + create_video_zip, ) from cms.djangoapps.contentstore.rest_api.v1.serializers import ( CourseVideosSerializer, VideoUsageSerializer, + VideoDownloadSerializer ) import cms.djangoapps.contentstore.toggles as contentstore_toggles @@ -180,3 +182,43 @@ def get(self, request: Request, course_id: str, edx_video_id: str): usage_locations = get_video_usage_path(course_key, edx_video_id) serializer = VideoUsageSerializer(usage_locations) return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class VideoDownloadView(DeveloperErrorViewMixin, APIView): + """ + View for course video downloads. + """ + @apidocs.schema( + body=VideoDownloadSerializer, + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + ], + responses={ + 200: "In case of success, a 200.", + 401: "The requester is not authenticated", + 403: "The requester cannot access the specified course", + 404: "The requested course does not exist", + }, + ) + @verify_course_exists() + def put(self, request: Request, course_id: str) -> Response: + """ + Get an object containing course videos. + **Example Request** + PUT /api/contentstore/v1/videos/{course_id}/download, { + "files": [ + {"url": 'someUrl.com', "name": 'test.mp4'} + ] + } + **Response Values** + If the request is successful, an HTTP 200 "OK" response is returned. + The HTTP 200 response contains a zip file attachment containing all the + requested videos. The returned file's name will be {course_id}_videos_{random_id}.zip. + """ + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + files = request.data['files'] + return create_video_zip(course_id, files) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 20c14089e0a6..e0bc9fcc9558 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -127,6 +127,17 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= dump_course_to_neo4j ) + # DEVELOPER README: probably all tasks here should use transaction.on_commit + # to avoid stale data, but the tasks are owned by many teams and are often + # working well enough. Several instead use a waiting strategy. + # If you are in here trying to figure out why your task is not working correctly, + # consider whether it is getting stale data and if so choose to wait for the transaction + # like exams or put your task to sleep for a while like discussions. + # You will not be able to replicate these errors in an environment where celery runs + # in process because it will be inside the transaction. Use the settings from + # devstack_with_worker.py, and consider adding a time.sleep into send_bulk_published_signal + # if you really want to make sure that the task happens before the data is ready. + # register special exams asynchronously after the data is ready course_key_str = str(course_key) transaction.on_commit(lambda: update_special_exams_and_publish.delay(course_key_str)) @@ -139,10 +150,9 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= # Push the course out to CourseGraph asynchronously. dump_course_to_neo4j.delay(course_key_str) - # Finally, call into the course search subsystem - # to kick off an indexing action + # Kick off a courseware indexing action after the data is ready if CoursewareSearchIndexer.indexing_is_enabled() and CourseAboutSearchIndexer.indexing_is_enabled(): - update_search_index.delay(course_key_str, datetime.now(UTC).isoformat()) + transaction.on_commit(lambda: update_search_index.delay(course_key_str, datetime.now(UTC).isoformat())) update_discussions_settings_from_course_task.apply_async( args=[course_key_str], diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 69c9057966ee..b8406ff61e20 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -464,7 +464,8 @@ def sync_discussion_settings(course_key, user): course.discussions_settings['provider_type'] = Provider.OPEN_EDX modulestore().update_item(course, user.id) - discussion_config.provider_type = Provider.OPEN_EDX + discussion_config.provider_type = Provider.OPEN_EDX + discussion_config.enable_graded_units = discussion_settings['enable_graded_units'] discussion_config.unit_level_visibility = discussion_settings['unit_level_visibility'] discussion_config.save() @@ -715,6 +716,7 @@ def read_chunk(): from .views.entrance_exam import add_entrance_exam_milestone add_entrance_exam_milestone(course.id, entrance_exam_chapter) LOGGER.info(f'Course import {course.id}: Entrance exam imported') + if is_course: sync_discussion_settings(courselike_key, user) @@ -895,14 +897,15 @@ def _create_copy_content_task(v2_library_key, v1_library_key): spin up a celery task to import the V1 Library's content into the V2 library. This utalizes the fact that course and v1 library content is stored almost identically. """ - return v2contentlib_api.import_blocks_create_task(v2_library_key, v1_library_key) + return v2contentlib_api.import_blocks_create_task( + v2_library_key, v1_library_key, + use_course_key_as_block_id_suffix=False + ) def _create_metadata(v1_library_key, collection_uuid): """instansiate an index for the V2 lib in the collection""" - print(collection_uuid) - store = modulestore() v1_library = store.get_library(v1_library_key) collection = get_collection(collection_uuid).uuid diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index 7d7a0d533b07..98a60dce901f 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -5,7 +5,7 @@ import time from datetime import datetime from unittest import skip -from unittest.mock import patch +from unittest.mock import patch, Mock import ddt import pytest @@ -585,6 +585,8 @@ def test_large_course_deletion(self): self._test_large_course_deletion(self.store) +@patch('cms.djangoapps.contentstore.signals.handlers.transaction.on_commit', + new=Mock(side_effect=lambda func: func()),) # run right away class TestTaskExecution(SharedModuleStoreTestCase): """ Set of tests to ensure that the task code will do the right thing when diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index e7dcc3647886..376ba56d8dd9 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -440,10 +440,6 @@ def test_sync_if_source_library_changed(self): html_block_2 = modulestore().get_item(lc_block.children[0]) self.assertEqual(html_block_2.data, data2) - @patch( - 'openedx.core.djangoapps.content_libraries.tasks.SearchEngine.get_search_engine', - Mock(return_value=None, autospec=True), - ) def test_sync_if_capa_type_changed(self): """ Tests that children are automatically refreshed if capa type field changes """ name1, name2 = "Option Problem", "Multiple Choice Problem" diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 8e3f97eee98e..61254cc6d29c 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -14,6 +14,7 @@ from django.urls import reverse from django.utils import translation from django.utils.translation import gettext as _ +from eventtracking import tracker from help_tokens.core import HelpUrlExpert from lti_consumer.models import CourseAllowPIISharingInLTIFlag from opaque_keys.edx.keys import CourseKey, UsageKey @@ -25,7 +26,8 @@ from xblock.fields import Scope from cms.djangoapps.contentstore.toggles import exam_setting_view_enabled -from common.djangoapps.course_action_state.models import CourseRerunUIStateManager +from common.djangoapps.course_action_state.models import CourseRerunUIStateManager, CourseRerunState +from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.edxmako.services import MakoService from common.djangoapps.student import auth @@ -36,6 +38,7 @@ CourseStaffRole, GlobalStaff, ) +from common.djangoapps.track import contexts from common.djangoapps.util.course import get_link_for_about_page from common.djangoapps.util.milestones_helpers import ( is_prerequisite_courses_enabled, @@ -45,6 +48,8 @@ get_namespace_choices, generate_milestone_namespace ) +from common.djangoapps.util.date_utils import get_default_time_display +from common.djangoapps.xblock_django.api import deprecated_xblocks from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core import toggles as core_toggles from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course @@ -77,9 +82,12 @@ use_new_video_uploads_page, use_new_custom_pages, use_tagging_taxonomy_list_page, + # use_xpert_translations_component, ) from cms.djangoapps.models.settings.course_grading import CourseGradingModel +from cms.djangoapps.models.settings.course_metadata import CourseMetadata from xmodule.library_tools import LibraryToolsService +from xmodule.course_block import DEFAULT_START_DATE # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order @@ -439,7 +447,7 @@ def get_taxonomy_list_url(): if use_tagging_taxonomy_list_page(): mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL if mfe_base_url: - taxonomy_list_url = f'{mfe_base_url}/taxonomy-list' + taxonomy_list_url = f'{mfe_base_url}/taxonomies' return taxonomy_list_url @@ -1471,44 +1479,17 @@ def get_library_context(request, request_is_json=False): return data -def get_home_context(request): +def get_course_context(request): """ - Utils is used to get context of course home. + Utils is used to get context of course home library tab. It is used for both DRF and django views. """ from cms.djangoapps.contentstore.views.course import ( - get_allowed_organizations, - get_allowed_organizations_for_libraries, get_courses_accessible_to_user, - user_can_create_organizations, - _accessible_libraries_iter, - _get_course_creator_status, - _format_library_for_view, _process_courses_list, ENABLE_GLOBAL_STAFF_OPTIMIZATION, ) - from cms.djangoapps.contentstore.views.library import ( - LIBRARY_AUTHORING_MICROFRONTEND_URL, - LIBRARIES_ENABLED, - should_redirect_to_library_authoring_mfe, - user_can_create_library, - ) - - optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() - - org = request.GET.get('org', '') if optimization_enabled else None - courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) - user = request.user - libraries = [] - response_format = get_response_format(request) - - if not split_library_view_on_dashboard() and LIBRARIES_ENABLED: - accessible_libraries = _accessible_libraries_iter(user) - libraries = [_format_library_for_view(lib, request) for lib in accessible_libraries] - - if split_library_view_on_dashboard() and request_response_format_is_json(request, response_format): - libraries = get_library_context(request, True)['libraries'] def format_in_process_course_view(uca): """ @@ -1531,9 +1512,52 @@ def format_in_process_course_view(uca): ) if uca.state == CourseRerunUIStateManager.State.FAILED else '' } + optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() + + org = request.GET.get('org', '') if optimization_enabled else None + courses_iter, in_process_course_actions = get_courses_accessible_to_user(request, org) split_archived = settings.FEATURES.get('ENABLE_SEPARATE_ARCHIVED_COURSES', False) active_courses, archived_courses = _process_courses_list(courses_iter, in_process_course_actions, split_archived) in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions] + return active_courses, archived_courses, in_process_course_actions + + +def get_home_context(request, no_course=False): + """ + Utils is used to get context of course home. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.course import ( + get_allowed_organizations, + get_allowed_organizations_for_libraries, + user_can_create_organizations, + _accessible_libraries_iter, + _get_course_creator_status, + _format_library_for_view, + ENABLE_GLOBAL_STAFF_OPTIMIZATION, + ) + from cms.djangoapps.contentstore.views.library import ( + LIBRARY_AUTHORING_MICROFRONTEND_URL, + LIBRARIES_ENABLED, + should_redirect_to_library_authoring_mfe, + user_can_create_library, + ) + + active_courses = [] + archived_courses = [] + in_process_course_actions = [] + + optimization_enabled = GlobalStaff().has_user(request.user) and ENABLE_GLOBAL_STAFF_OPTIMIZATION.is_enabled() + + user = request.user + libraries = [] + + if not no_course: + active_courses, archived_courses, in_process_course_actions = get_course_context(request) + + if not split_library_view_on_dashboard() and LIBRARIES_ENABLED and not no_course: + libraries = get_library_context(request, True)['libraries'] home_context = { 'courses': active_courses, @@ -1595,6 +1619,7 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): get_transcript_preferences, ) from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag + from openedx.core.djangoapps.video_config.toggles import use_xpert_translations_component from xmodule.video_block.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order from .video_storage_handlers import ( @@ -1619,6 +1644,7 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): course = modulestore().get_course(course_key) is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id) + is_ai_translations_enabled = use_xpert_translations_component(course.id) previous_uploads, pagination_context = _get_index_videos(course, pagination_conf) course_video_context = { 'context_course': course, @@ -1639,6 +1665,7 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): 'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS }, 'is_video_transcript_enabled': is_video_transcript_enabled, + 'is_ai_translations_enabled': is_ai_translations_enabled, 'active_transcript_preferences': None, 'transcript_credentials': None, 'transcript_available_languages': get_all_transcript_languages(), @@ -1668,6 +1695,97 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): return course_video_context +def get_course_index_context(request, course_key, course_block=None): + """ + Wrapper function to wrap _get_course_index_context in bulk operation + if course_block is None. + """ + if not course_block: + with modulestore().bulk_operations(course_key): + course_block = modulestore().get_course(course_key) + return _get_course_index_context(request, course_key, course_block) + return _get_course_index_context(request, course_key, course_block) + + +def _get_course_index_context(request, course_key, course_block): + """ + Utils is used to get context of course index outline. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.course import ( + course_outline_initial_state, + _course_outline_json, + _deprecated_blocks_info, + ) + from openedx.core.djangoapps.content_staging import api as content_staging_api + + lms_link = get_lms_link_for_item(course_block.location) + reindex_link = None + if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): + if GlobalStaff().has_user(request.user): + reindex_link = f"/course/{str(course_key)}/search_reindex" + sections = course_block.get_children() + course_structure = _course_outline_json(request, course_block) + locator_to_show = request.GET.get('show', None) + + course_release_date = ( + get_default_time_display(course_block.start) + if course_block.start != DEFAULT_START_DATE + else _("Set Date") + ) + + settings_url = reverse_course_url('settings_handler', course_key) + + try: + current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True) + except (ItemNotFoundError, CourseActionStateItemNotFoundError): + current_action = None + + deprecated_block_names = [block.name for block in deprecated_xblocks()] + deprecated_blocks_info = _deprecated_blocks_info(course_block, deprecated_block_names) + + frontend_app_publisher_url = configuration_helpers.get_value_for_org( + course_block.location.org, + 'FRONTEND_APP_PUBLISHER_URL', + settings.FEATURES.get('FRONTEND_APP_PUBLISHER_URL', False) + ) + # gather any errors in the currently stored proctoring settings. + advanced_dict = CourseMetadata.fetch(course_block) + proctoring_errors = CourseMetadata.validate_proctoring_settings(course_block, advanced_dict, request.user) + + user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) + + course_index_context = { + 'language_code': request.LANGUAGE_CODE, + 'context_course': course_block, + 'lms_link': lms_link, + 'sections': sections, + 'course_structure': course_structure, + 'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None, # lint-amnesty, pylint: disable=line-too-long + 'initial_user_clipboard': user_clipboard, + 'rerun_notification_id': current_action.id if current_action else None, + 'course_release_date': course_release_date, + 'settings_url': settings_url, + 'reindex_link': reindex_link, + 'deprecated_blocks_info': deprecated_blocks_info, + 'notification_dismiss_url': reverse_course_url( + 'course_notifications_handler', + current_action.course_key, + kwargs={ + 'action_state_id': current_action.id, + }, + ) if current_action else None, + 'frontend_app_publisher_url': frontend_app_publisher_url, + 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_block.id), + 'advance_settings_url': reverse_course_url('advanced_settings_handler', course_block.id), + 'proctoring_errors': proctoring_errors, + 'taxonomy_tags_widget_url': get_taxonomy_tags_widget_url(course_block.id), + } + + return course_index_context + + class StudioPermissionsService: """ Service that can provide information about a user's permissions. @@ -1687,3 +1805,21 @@ def can_read(self, course_key): def can_write(self, course_key): """ Does the user have read access to the given course/library? """ return has_studio_write_access(self._user, course_key) + + +def track_course_update_event(course_key, user, event_data=None): + """ + Track course update event + """ + event_name = 'edx.contentstore.course_update' + event_data['course_id'] = str(course_key) + event_data['user_id'] = str(user.id) + event_data['user_forums_roles'] = [ + role.name for role in user.roles.filter(course_id=str(course_key)) + ] + event_data['user_course_roles'] = [ + role.role for role in user.courseaccessrole_set.filter(course_id=str(course_key)) + ] + context = contexts.course_context_from_course_id(course_key) + with tracker.get_tracker().context(event_name, context): + tracker.emit(event_name, event_data) diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index 4d546aab3de5..ce3fe43461bb 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -8,6 +8,12 @@ import io import json import logging +import os +import requests +import shutil +import pathlib +import zipfile + from contextlib import closing from datetime import datetime, timedelta from uuid import uuid4 @@ -15,7 +21,7 @@ from boto import s3 from django.conf import settings from django.contrib.staticfiles.storage import staticfiles_storage -from django.http import FileResponse, HttpResponseNotFound +from django.http import FileResponse, HttpResponseNotFound, StreamingHttpResponse from django.shortcuts import redirect from django.utils.translation import gettext as _ from django.utils.translation import gettext_noop @@ -35,10 +41,14 @@ update_video_image, update_video_status ) +from fs.osfs import OSFS from opaque_keys.edx.keys import CourseKey +from path import Path as path from pytz import UTC from rest_framework import status as rest_status from rest_framework.response import Response +from tempfile import NamedTemporaryFile, mkdtemp +from wsgiref.util import FileWrapper from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.util.json_request import JsonResponse @@ -221,6 +231,53 @@ def handle_videos(request, course_key_string, edx_video_id=None): return JsonResponse(data, status=status) +def send_zip(zip_file, size=None): + """ + Generates a streaming http response for the zip file + """ + wrapper = FileWrapper(zip_file, settings.COURSE_EXPORT_DOWNLOAD_CHUNK_SIZE) + response = StreamingHttpResponse(wrapper, content_type='application/zip') + response['Content-Dispositon'] = 'attachment; filename=%s' % os.path.basename(zip_file.name) + response['Content-Length'] = size + return response + + +def create_video_zip(course_key_string, files): + """ + Generates the video zip, or returns None if there was an error. + + Updates the context with any error information if applicable. + """ + name = course_key_string + '_videos' + video_folder_zip = NamedTemporaryFile(prefix=name + '_', + suffix=".zip") # lint-amnesty, pylint: disable=consider-using-with + root_dir = path(mkdtemp()) + video_dir = root_dir + '/' + name + zip_folder = None + try: + for file in files: + url = file['url'] + file_name = file['name'] + response = requests.get(url, allow_redirects=True) + file_type = '.' + response.headers['Content-Type'][6:] + if file_type not in file_name: + file_name = file['name'] + file_type + if not os.path.isdir(video_dir): + os.makedirs(video_dir) + with OSFS(video_dir).open(file_name, mode="wb") as f: + f.write(response.content) + directory = pathlib.Path(video_dir) + with zipfile.ZipFile(video_folder_zip, mode="w") as archive: + for file_path in directory.iterdir(): + archive.write(file_path, arcname=file_path.name) + zip_folder = open(video_folder_zip.name, '+rb') + + return send_zip(zip_folder, video_folder_zip.tell()) + finally: + if os.path.exists(root_dir / name): + shutil.rmtree(root_dir / name) + + def get_video_usage_path(course_key, edx_video_id): """ API for fetching the locations a specific video is used in a course. @@ -234,15 +291,26 @@ def get_video_usage_path(course_key, edx_video_id): 'category': 'video' }, ) + for video in videos: video_id = getattr(video, 'edx_video_id', '') - if video_id == edx_video_id: - unit = video.get_parent() - subsection = unit.get_parent() - subsection_display_name = getattr(subsection, 'display_name', '') - unit_display_name = getattr(unit, 'display_name', '') - xblock_display_name = getattr(video, 'display_name', '') - usage_locations.append(f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}') + try: + if video_id == edx_video_id: + usage_dict = {'display_location': '', 'url': ''} + video_location = str(video.location) + xblock_display_name = getattr(video, 'display_name', '') + unit = video.get_parent() + unit_location = str(video.parent) + unit_display_name = getattr(unit, 'display_name', '') + subsection = unit.get_parent() + subsection_display_name = getattr(subsection, 'display_name', '') + usage_dict['display_location'] = (f'{subsection_display_name} - ' + f'{unit_display_name} / {xblock_display_name}') + usage_dict['url'] = f'/container/{unit_location}#{video_location}' + usage_locations.append(usage_dict) + except AttributeError: + continue + return {'usage_locations': usage_locations} @@ -617,7 +685,6 @@ def _get_values(video, course): Get data for predefined video attributes. """ values = {} - values["usage_locations"] = get_video_usage_path(course.id, video["edx_video_id"])['usage_locations'] for attr in attrs: if attr == 'courses': current_course = [c for c in video['courses'] if course_id in c] @@ -666,12 +733,12 @@ def videos_index_html(course, pagination_conf=None): """ Returns an HTML page to display previous video uploads and allow new ones """ + if use_new_video_uploads_page(course.id): + return redirect(get_video_uploads_url(course.id)) context = get_course_videos_context( course, pagination_conf, ) - if use_new_video_uploads_page(course.id): - return redirect(get_video_uploads_url(course.id)) return render_to_response('videos_index.html', context) diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index 91078ccd11c6..8d157c66c45b 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -28,7 +28,7 @@ from xmodule.modulestore.django import ( modulestore, ) # lint-amnesty, pylint: disable=wrong-import-order - +from cms.djangoapps.contentstore.toggles import use_tagging_taxonomy_list_page from xmodule.x_module import ( AUTHOR_VIEW, @@ -51,7 +51,10 @@ get_xblock, delete_orphans, ) -from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import usage_key_with_run +from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import ( + usage_key_with_run, + get_children_tags_count, +) __all__ = [ @@ -230,6 +233,11 @@ def xblock_view_handler(request, usage_key_string, view_name): force_render = request.GET.get("force_render", None) + # Fetch tags of children components + tags_count_map = {} + if use_tagging_taxonomy_list_page(): + tags_count_map = get_children_tags_count(xblock) + # Set up the context to be passed to each XBlock's render method. context = request.GET.dict() context.update( @@ -245,6 +253,7 @@ def xblock_view_handler(request, usage_key_string, view_name): "paging": paging, "force_render": force_render, "item_url": "/container/{usage_key}", + "tags_count_map": tags_count_map, } ) fragment = get_preview_fragment(request, xblock, context) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index b42c011c4b20..eac3c1048d7a 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -54,12 +54,9 @@ UserBasedRole, OrgStaffRole ) -from common.djangoapps.util.date_utils import get_default_time_display from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json from common.djangoapps.util.string_utils import _has_non_ascii_characters -from common.djangoapps.xblock_django.api import deprecated_xblocks from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.content_staging import api as content_staging_api from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers @@ -69,11 +66,11 @@ from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME from organizations.models import Organization from xmodule.contentstore.content import StaticContent # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.course_block import CourseBlock, DEFAULT_START_DATE, CourseFields # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.course_block import CourseBlock, CourseFields # lint-amnesty, pylint: disable=wrong-import-order from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.exceptions import DuplicateCourseError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.partitions.partitions import UserPartition # lint-amnesty, pylint: disable=wrong-import-order from xmodule.tabs import CourseTab, CourseTabList, InvalidTabsException # lint-amnesty, pylint: disable=wrong-import-order @@ -102,10 +99,10 @@ get_course_grading, get_home_context, get_library_context, + get_course_index_context, get_lms_link_for_item, get_proctored_exam_settings_url, get_course_outline_url, - get_taxonomy_tags_widget_url, get_studio_home_url, get_updates_url, get_advanced_settings_url, @@ -621,76 +618,17 @@ def course_index(request, course_key): org, course, name: Attributes of the Location for the item to edit """ - # A depth of None implies the whole course. The course outline needs this in order to compute has_changes. - # A unit may not have a draft version, but one of its components could, and hence the unit itself has changes. + if use_new_course_outline_page(course_key): + return redirect(get_course_outline_url(course_key)) with modulestore().bulk_operations(course_key): + # A depth of None implies the whole course. The course outline needs this in order to compute has_changes. + # A unit may not have a draft version, but one of its components could, and hence the unit itself has changes. course_block = get_course_and_check_access(course_key, request.user, depth=None) if not course_block: raise Http404 - if use_new_course_outline_page(course_key): - return redirect(get_course_outline_url(course_key)) - lms_link = get_lms_link_for_item(course_block.location) - reindex_link = None - if settings.FEATURES.get('ENABLE_COURSEWARE_INDEX', False): - if GlobalStaff().has_user(request.user): - reindex_link = f"/course/{str(course_key)}/search_reindex" - sections = course_block.get_children() - course_structure = _course_outline_json(request, course_block) - locator_to_show = request.GET.get('show', None) - - course_release_date = ( - get_default_time_display(course_block.start) - if course_block.start != DEFAULT_START_DATE - else _("Set Date") - ) - - settings_url = reverse_course_url('settings_handler', course_key) - - try: - current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True) - except (ItemNotFoundError, CourseActionStateItemNotFoundError): - current_action = None - - deprecated_block_names = [block.name for block in deprecated_xblocks()] - deprecated_blocks_info = _deprecated_blocks_info(course_block, deprecated_block_names) - - frontend_app_publisher_url = configuration_helpers.get_value_for_org( - course_block.location.org, - 'FRONTEND_APP_PUBLISHER_URL', - settings.FEATURES.get('FRONTEND_APP_PUBLISHER_URL', False) - ) - # gather any errors in the currently stored proctoring settings. - advanced_dict = CourseMetadata.fetch(course_block) - proctoring_errors = CourseMetadata.validate_proctoring_settings(course_block, advanced_dict, request.user) - - user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) - - return render_to_response('course_outline.html', { - 'language_code': request.LANGUAGE_CODE, - 'context_course': course_block, - 'lms_link': lms_link, - 'sections': sections, - 'course_structure': course_structure, - 'initial_state': course_outline_initial_state(locator_to_show, course_structure) if locator_to_show else None, # lint-amnesty, pylint: disable=line-too-long - 'initial_user_clipboard': user_clipboard, - 'rerun_notification_id': current_action.id if current_action else None, - 'course_release_date': course_release_date, - 'settings_url': settings_url, - 'reindex_link': reindex_link, - 'deprecated_blocks_info': deprecated_blocks_info, - 'notification_dismiss_url': reverse_course_url( - 'course_notifications_handler', - current_action.course_key, - kwargs={ - 'action_state_id': current_action.id, - }, - ) if current_action else None, - 'frontend_app_publisher_url': frontend_app_publisher_url, - 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_block.id), - 'advance_settings_url': reverse_course_url('advanced_settings_handler', course_block.id), - 'proctoring_errors': proctoring_errors, - 'taxonomy_tags_widget_url': get_taxonomy_tags_widget_url(course_block.id), - }) + # should be under bulk_operations if course_block is passed + course_index_context = get_course_index_context(request, course_key, course_block) + return render_to_response('course_outline.html', course_index_context) @function_trace('get_courses_accessible_to_user') diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index bb1616c04eb2..117939741929 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -91,7 +91,8 @@ def import_handler(request, course_key_string): else: return _write_chunk(request, courselike_key) elif request.method == 'GET': # assume html - if use_new_import_page(courselike_key): + + if use_new_import_page(courselike_key) and not library: return redirect(get_import_url(courselike_key)) status_url = reverse_course_url( "import_status_handler", courselike_key, kwargs={'filename': "fillerName"} @@ -314,8 +315,8 @@ def export_handler(request, course_key_string): course_key = CourseKey.from_string(course_key_string) if not has_course_author_access(request.user, course_key): raise PermissionDenied() - - if isinstance(course_key, LibraryLocator): + library = isinstance(course_key, LibraryLocator) + if library: courselike_block = modulestore().get_library(course_key) context = { 'context_library': courselike_block, @@ -340,7 +341,7 @@ def export_handler(request, course_key_string): export_olx.delay(request.user.id, course_key_string, request.LANGUAGE_CODE) return JsonResponse({'ExportStatus': 1}) elif 'text/html' in requested_format: - if use_new_export_page(course_key): + if use_new_export_page(course_key) and not library: return redirect(get_export_url(course_key)) return render_to_response('export.html', context) else: diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index acab35471813..a897a38ad21a 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -302,6 +302,10 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): can_edit = context.get('can_edit', True) # Is this a course or a library? is_course = xblock.scope_ids.usage_id.context_key.is_course + tags_count_map = context.get('tags_count_map') + tags_count = 0 + if tags_count_map: + tags_count = tags_count_map.get(str(xblock.location), 0) template_context = { 'xblock_context': context, 'xblock': xblock, @@ -318,7 +322,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): 'can_add': context.get('can_add', True), 'can_move': context.get('can_move', is_course), 'language': getattr(course, 'language', None), - 'is_course': is_course + 'is_course': is_course, + 'tags_count': tags_count, } add_webpack_js_to_fragment(frag, "js/factories/xblock_validation") diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index a90187ef605d..ac86961b2293 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -17,6 +17,7 @@ from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED from openedx_events.tests.utils import OpenEdxEventsTestMixin from edx_proctoring.exceptions import ProctoredExamNotFoundException +from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys import InvalidKeyError from opaque_keys.edx.asides import AsideUsageKeyV2 from opaque_keys.edx.keys import CourseKey, UsageKey @@ -83,6 +84,7 @@ add_container_page_publishing_info, create_xblock_info, ) +from cms.djangoapps.contentstore.toggles import ENABLE_TAGGING_TAXONOMY_LIST_PAGE class AsideTest(XBlockAside): @@ -269,6 +271,37 @@ def test_get_container_nested_container_fragment(self): ), ) + @override_waffle_flag(ENABLE_TAGGING_TAXONOMY_LIST_PAGE, True) + @patch("cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers.get_object_tag_counts") + def test_tag_count_in_container_fragment(self, mock_get_object_tag_counts): + root_usage_key = self._create_vertical() + + # Add a problem beneath a child vertical + child_vertical_usage_key = self._create_vertical( + parent_usage_key=root_usage_key + ) + resp = self.create_xblock( + parent_usage_key=child_vertical_usage_key, + category="problem", + boilerplate="multiplechoice.yaml", + ) + self.assertEqual(resp.status_code, 200) + usage_key = self.response_usage_key(resp) + + # Get the preview HTML without tags + mock_get_object_tag_counts.return_value = {} + html, __ = self._get_container_preview(root_usage_key) + self.assertIn("wrapper-xblock", html) + self.assertNotIn('data-testid="tag-count-button"', html) + + # Get the preview HTML with tags + mock_get_object_tag_counts.return_value = { + str(usage_key): 13 + } + html, __ = self._get_container_preview(root_usage_key) + self.assertIn("wrapper-xblock", html) + self.assertIn('data-testid="tag-count-button"', html) + def test_split_test(self): """ Test that a split_test block renders all of its children in Studio. diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 429630ac8d1b..011c9a10e561 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -3,16 +3,25 @@ allow users to paste XBlocks that were copied using the staged_content/clipboard APIs. """ +import ddt +from django.test import LiveServerTestCase from opaque_keys.edx.keys import UsageKey from rest_framework.test import APIClient -from xmodule.modulestore.django import contentstore +from organizations.models import Organization +from xmodule.modulestore.django import contentstore, modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, upload_file_to_course from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, ToyCourseFactory +from cms.djangoapps.contentstore.utils import reverse_usage_url +from openedx.core.lib.blockstore_api.tests.base import BlockstoreAppTestMixin +from openedx.core.djangoapps.content_libraries import api as library_api +from blockstore.apps import api as blockstore_api + CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/" XBLOCK_ENDPOINT = "/xblock/" +@ddt.ddt class ClipboardPasteTestCase(ModuleStoreTestCase): """ Test Clipboard Paste functionality @@ -99,6 +108,42 @@ def test_copy_and_paste_unit(self): # The new block should store a reference to where it was copied from assert dest_unit.copied_from_block == str(unit_key) + @ddt.data( + # A problem with absolutely no fields set. A previous version of copy-paste had an error when pasting this. + {"category": "problem", "display_name": None, "data": ""}, + {"category": "problem", "display_name": "Emoji Land 😎", "data": "emoji in the body 😎"}, + ) + def test_copy_and_paste_component(self, block_args): + """ + Test copying a component (XBlock) from one course into another + """ + source_course = CourseFactory.create(display_name='Source Course') + source_block = BlockFactory.create(parent_location=source_course.location, **block_args) + + dest_course = CourseFactory.create(display_name='Destination Course') + with self.store.bulk_operations(dest_course.id): + dest_chapter = BlockFactory.create(parent=dest_course, category='chapter', display_name='Section') + dest_sequential = BlockFactory.create(parent=dest_chapter, category='sequential', display_name='Subsection') + + # Copy the block + client = APIClient() + client.login(username=self.user.username, password=self.user_password) + copy_response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(source_block.location)}, format="json") + assert copy_response.status_code == 200 + + # Paste the unit + paste_response = client.post(XBLOCK_ENDPOINT, { + "parent_locator": str(dest_sequential.location), + "staged_content": "clipboard", + }, format="json") + assert paste_response.status_code == 200 + dest_block_key = UsageKey.from_string(paste_response.json()["locator"]) + + dest_block = self.store.get_item(dest_block_key) + assert dest_block.display_name == source_block.display_name + # The new block should store a reference to where it was copied from + assert dest_block.copied_from_block == str(source_block.location) + def test_paste_with_assets(self): """ When pasting into a different course, any required static assets should @@ -167,3 +212,97 @@ def test_paste_with_assets(self): source_pic2_hash = contentstore().find(source_course.id.make_asset_key("asset", "picture2.jpg")).content_digest dest_pic2_hash = contentstore().find(dest_course_key.make_asset_key("asset", "picture2.jpg")).content_digest assert source_pic2_hash != dest_pic2_hash # Because there was a conflict, this file was unchanged. + + +class ClipboardLibraryContentPasteTestCase(BlockstoreAppTestMixin, LiveServerTestCase, ModuleStoreTestCase): + """ + Test Clipboard Paste functionality with library content + """ + + def setUp(self): + """ + Set up a v2 Content Library and a library content block + """ + super().setUp() + self.client = APIClient() + self.client.login(username=self.user.username, password=self.user_password) + self.store = modulestore() + # Create a content library: + library = library_api.create_library( + collection_uuid=blockstore_api.create_collection("Collection").uuid, + library_type=library_api.COMPLEX, + org=Organization.objects.create(name="Test Org", short_name="CL-TEST"), + slug="lib", + title="Library", + ) + # Populate it with a problem: + problem_key = library_api.create_library_block(library.key, "problem", "p1").usage_key + library_api.set_library_block_olx(problem_key, """ + + + + + Wrong + Right + + + + """) + library_api.publish_changes(library.key) + + # Create a library content block (lc), point it out our library, and sync it. + self.course = CourseFactory.create(display_name='Course') + self.orig_lc_block = BlockFactory.create( + parent=self.course, + category="library_content", + source_library_id=str(library.key), + display_name="LC Block", + publish_item=False, + ) + self.dest_lc_block = None + + self._sync_lc_block_from_library('orig_lc_block') + orig_child = self.store.get_item(self.orig_lc_block.children[0]) + assert orig_child.display_name == "MCQ" + + def test_paste_library_content_block(self): + """ + Test the special handling of copying and pasting library content + """ + # Copy a library content block that has children: + copy_response = self.client.post(CLIPBOARD_ENDPOINT, { + "usage_key": str(self.orig_lc_block.location) + }, format="json") + assert copy_response.status_code == 200 + + # Paste the Library content block: + paste_response = self.client.post(XBLOCK_ENDPOINT, { + "parent_locator": str(self.course.location), + "staged_content": "clipboard", + }, format="json") + assert paste_response.status_code == 200 + dest_lc_block_key = UsageKey.from_string(paste_response.json()["locator"]) + + # Get the ID of the new child: + self.dest_lc_block = self.store.get_item(dest_lc_block_key) + dest_child = self.store.get_item(self.dest_lc_block.children[0]) + assert dest_child.display_name == "MCQ" + + # Importantly, the ID of the child must not changed when the library content is synced. + # Otherwise, user state saved against this child will be lost when it syncs. + self._sync_lc_block_from_library('dest_lc_block') + updated_dest_child = self.store.get_item(self.dest_lc_block.children[0]) + assert dest_child.location == updated_dest_child.location + + def _sync_lc_block_from_library(self, attr_name): + """ + Helper method to "sync" a Library Content Block by [re-]fetching its + children from the library. + """ + usage_key = getattr(self, attr_name).location + # It's easiest to do this via the REST API: + handler_url = reverse_usage_url('preview_handler', usage_key, kwargs={'handler': 'upgrade_and_sync'}) + response = self.client.post(handler_url) + assert response.status_code == 200 + # Now reload the block and make sure the child is in place + setattr(self, attr_name, self.store.get_item(usage_key)) # we must reload after upgrade_and_sync diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 8a01d3cfffea..b30f8c95a631 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -6,7 +6,6 @@ import datetime import json from unittest import mock, skip -from unittest.mock import patch import ddt import lxml @@ -644,7 +643,7 @@ def test_verify_warn_only_on_enabled_blocks(self, enabled_block_types, deprecate ) @override_settings(FEATURES={'ENABLE_EXAM_SETTINGS_HTML_VIEW': True}) - @patch('cms.djangoapps.models.settings.course_metadata.CourseMetadata.validate_proctoring_settings') + @mock.patch('cms.djangoapps.models.settings.course_metadata.CourseMetadata.validate_proctoring_settings') def test_proctoring_link_is_visible(self, mock_validate_proctoring_settings): """ Test to check proctored exam settings mfe url is rendering properly @@ -665,6 +664,14 @@ def test_proctoring_link_is_visible(self, mock_validate_proctoring_settings): proctored_exam_settings_url = get_proctored_exam_settings_url(self.course.id) self.assertContains(response, proctored_exam_settings_url, 2) + def test_number_of_calls_to_db(self): + """ + Test to check number of queries made to mysql and mongo + """ + with self.assertNumQueries(26, table_ignorelist=WAFFLE_TABLES): + with check_mongo_calls(3): + self.client.get_html(reverse_course_url('course_handler', self.course.id)) + class TestCourseReIndex(CourseTestCase): """ @@ -677,9 +684,11 @@ class TestCourseReIndex(CourseTestCase): ENABLED_SIGNALS = ['course_published'] + @mock.patch('cms.djangoapps.contentstore.signals.handlers.transaction.on_commit', + new=mock.Mock(side_effect=lambda func: func()), ) # run index right away def setUp(self): """ - Set up the for the course outline tests. + Set up the for the course reindex tests. """ super().setUp() diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index 1eadd49c50a8..2e6e6aef254c 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -374,7 +374,6 @@ def test_get_json(self): 'transcription_status', 'transcript_urls', 'error_description', - 'usage_locations' } ) dateutil.parser.parse(response_video['created']) @@ -391,7 +390,7 @@ def test_get_json(self): 'edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url', 'file_size', 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', - 'error_description', 'usage_locations' + 'error_description' ], [ { @@ -409,7 +408,7 @@ def test_get_json(self): 'edx_video_id', 'client_video_id', 'created', 'duration', 'status', 'course_video_image_url', 'file_size', 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', - 'error_description', 'usage_locations' + 'error_description' ], [ { diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index f82a6f599d11..eb79181c4659 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -10,7 +10,6 @@ """ import logging from datetime import datetime -from uuid import uuid4 from attrs import asdict from django.conf import settings @@ -18,12 +17,9 @@ from django.contrib.auth.models import User # pylint: disable=imported-auth-user from django.core.exceptions import PermissionDenied from django.http import HttpResponse, HttpResponseBadRequest -from django.utils.timezone import timezone from django.utils.translation import gettext as _ from edx_django_utils.plugins import pluggable_override -from openedx_events.content_authoring.data import DuplicatedXBlockData -from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED -from openedx_tagging.core.tagging import api as tagging_api +from openedx.core.djangoapps.content_tagging.api import get_object_tag_counts from edx_proctoring.api import ( does_backend_support_onboarding, get_exam_by_content_id, @@ -31,7 +27,6 @@ ) from edx_proctoring.exceptions import ProctoredExamNotFoundException from help_tokens.core import HelpUrlExpert -from lti_consumer.models import CourseAllowPIISharingInLTIFlag from opaque_keys.edx.locator import LibraryUsageLocator from pytz import UTC from xblock.core import XBlock @@ -41,7 +36,6 @@ from cms.djangoapps.contentstore.toggles import ENABLE_COPY_PASTE_UNITS, use_tagging_taxonomy_list_page from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig -from common.djangoapps.edxmako.services import MakoService from common.djangoapps.static_replace import replace_static_urls from common.djangoapps.student.auth import ( has_studio_read_access, @@ -49,7 +43,6 @@ ) from common.djangoapps.util.date_utils import get_default_time_display from common.djangoapps.util.json_request import JsonResponse, expect_json -from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core.djangoapps.bookmarks import api as bookmarks_api from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE @@ -57,13 +50,11 @@ from openedx.core.lib.cache_utils import request_cached from openedx.core.toggles import ENTRANCE_EXAMS from xmodule.course_block import DEFAULT_START_DATE -from xmodule.library_tools import LibraryToolsService from xmodule.modulestore import EdxJSONEncoder, ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata -from xmodule.services import ConfigurationService, SettingsService, TeamsConfigurationService from xmodule.tabs import CourseTabList from ..utils import ( @@ -77,6 +68,8 @@ is_currently_visible_to_students, is_self_paced, get_taxonomy_tags_widget_url, + load_services_for_studio, + duplicate_block, ) from .create_xblock import create_xblock @@ -228,11 +221,11 @@ def handle_xblock(request, usage_key_string=None): status=400, ) - dest_usage_key = _duplicate_block( + dest_usage_key = duplicate_block( parent_usage_key, duplicate_source_usage_key, request.user, - request.json.get("display_name"), + display_name=request.json.get('display_name'), ) return JsonResponse( @@ -296,46 +289,6 @@ def modify_xblock(usage_key, request): ) -class StudioPermissionsService: - """ - Service that can provide information about a user's permissions. - - Deprecated. To be replaced by a more general authorization service. - - Only used by LibraryContentBlock (and library_tools.py). - """ - - def __init__(self, user): - self._user = user - - def can_read(self, course_key): - """Does the user have read access to the given course/library?""" - return has_studio_read_access(self._user, course_key) - - def can_write(self, course_key): - """Does the user have read access to the given course/library?""" - return has_studio_write_access(self._user, course_key) - - -def load_services_for_studio(runtime, user): - """ - Function to set some required services used for XBlock edits and studio_view. - (i.e. whenever we're not loading _prepare_runtime_for_preview.) This is required to make information - about the current user (especially permissions) available via services as needed. - """ - services = { - "user": DjangoXBlockUserService(user), - "studio_user_permissions": StudioPermissionsService(user), - "mako": MakoService(), - "settings": SettingsService(), - "lti-configuration": ConfigurationService(CourseAllowPIISharingInLTIFlag), - "teams_configuration": TeamsConfigurationService(), - "library_tools": LibraryToolsService(modulestore(), user.id), - } - - runtime._services.update(services) # lint-amnesty, pylint: disable=protected-access - - def _update_with_callback(xblock, user, old_metadata=None, old_content=None): """ Updates the xblock in the modulestore. @@ -786,129 +739,6 @@ def _move_item(source_usage_key, target_parent_usage_key, user, target_index=Non return JsonResponse(context) -def _duplicate_block( - parent_usage_key, - duplicate_source_usage_key, - user, - display_name=None, - is_child=False, -): - """ - Duplicate an existing xblock as a child of the supplied parent_usage_key. - """ - store = modulestore() - with store.bulk_operations(duplicate_source_usage_key.course_key): - source_item = store.get_item(duplicate_source_usage_key) - # Change the blockID to be unique. - dest_usage_key = source_item.location.replace(name=uuid4().hex) - category = dest_usage_key.block_type - - # Update the display name to indicate this is a duplicate (unless display name provided). - # Can't use own_metadata(), b/c it converts data for JSON serialization - - # not suitable for setting metadata of the new block - duplicate_metadata = {} - for field in source_item.fields.values(): - if field.scope == Scope.settings and field.is_set_on(source_item): - duplicate_metadata[field.name] = field.read_from(source_item) - - if is_child: - display_name = ( - display_name or source_item.display_name or source_item.category - ) - - if display_name is not None: - duplicate_metadata["display_name"] = display_name - else: - if source_item.display_name is None: - duplicate_metadata["display_name"] = _("Duplicate of {0}").format( - source_item.category - ) - else: - duplicate_metadata["display_name"] = _("Duplicate of '{0}'").format( - source_item.display_name - ) - - asides_to_create = [] - for aside in source_item.runtime.get_asides(source_item): - for field in aside.fields.values(): - if field.scope in ( - Scope.settings, - Scope.content, - ) and field.is_set_on(aside): - asides_to_create.append(aside) - break - - for aside in asides_to_create: - for field in aside.fields.values(): - if field.scope not in ( - Scope.settings, - Scope.content, - ): - field.delete_from(aside) - - dest_block = store.create_item( - user.id, - dest_usage_key.course_key, - dest_usage_key.block_type, - block_id=dest_usage_key.block_id, - definition_data=source_item.get_explicitly_set_fields_by_scope( - Scope.content - ), - metadata=duplicate_metadata, - runtime=source_item.runtime, - asides=asides_to_create, - ) - - children_handled = False - - if hasattr(dest_block, "studio_post_duplicate"): - # Allow an XBlock to do anything fancy it may need to when duplicated from another block. - # These blocks may handle their own children or parenting if needed. Let them return booleans to - # let us know if we need to handle these or not. - # TODO: Make this a proper method in the base class so we don't need getattr. - # See https://github.com/openedx/edx-platform/issues/33715 - load_services_for_studio(dest_block.runtime, user) - children_handled = dest_block.studio_post_duplicate(store, source_item) - - # Children are not automatically copied over (and not all xblocks have a 'children' attribute). - # Because DAGs are not fully supported, we need to actually duplicate each child as well. - if source_item.has_children and not children_handled: - dest_block.children = dest_block.children or [] - for child in source_item.children: - dupe = _duplicate_block( - dest_block.location, child, user=user, is_child=True - ) - if ( - dupe not in dest_block.children - ): # _duplicate_block may add the child for us. - dest_block.children.append(dupe) - store.update_item(dest_block, user.id) - - # pylint: disable=protected-access - if "detached" not in source_item.runtime.load_block_type(category)._class_tags: - parent = store.get_item(parent_usage_key) - # If source was already a child of the parent, add duplicate immediately afterward. - # Otherwise, add child to end. - if source_item.location in parent.children: - source_index = parent.children.index(source_item.location) - parent.children.insert(source_index + 1, dest_block.location) - else: - parent.children.append(dest_block.location) - store.update_item(parent, user.id) - - # .. event_implemented_name: XBLOCK_DUPLICATED - XBLOCK_DUPLICATED.send_event( - time=datetime.now(timezone.utc), - xblock_info=DuplicatedXBlockData( - usage_key=dest_block.location, - block_type=dest_block.location.block_type, - source_usage_key=duplicate_source_usage_key, - ), - ) - - return dest_block.location - - @login_required def delete_item(request, usage_key): """ @@ -1387,9 +1217,6 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements else: xblock_info["staff_only_message"] = False - # If the ENABLE_COPY_PASTE_UNITS feature flag is enabled, we show the newer menu that allows copying/pasting - xblock_info["enable_copy_paste_units"] = ENABLE_COPY_PASTE_UNITS.is_enabled() - # If the ENABLE_TAGGING_TAXONOMY_LIST_PAGE feature flag is enabled, we show the "Manage Tags" options if use_tagging_taxonomy_list_page(): xblock_info["use_tagging_taxonomy_list_page"] = True @@ -1402,6 +1229,10 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements xblock, course=course ) + if course_outline or is_xblock_unit: + # If the ENABLE_COPY_PASTE_UNITS feature flag is enabled, we show the newer menu that allows copying/pasting + xblock_info["enable_copy_paste_units"] = ENABLE_COPY_PASTE_UNITS.is_enabled() + if is_xblock_unit and summary_configuration.is_enabled(): xblock_info["summary_configuration_enabled"] = summary_configuration.is_summary_enabled(xblock_info['id']) @@ -1418,7 +1249,7 @@ def _get_course_unit_tags(course_key) -> dict: # Create a pattern to match the IDs of the units, e.g. "block-v1:org+course+run+type@vertical+block@*" vertical_key = course_key.make_usage_key('vertical', 'x') unit_key_pattern = str(vertical_key).rsplit("@", 1)[0] + "@*" - return tagging_api.get_object_tag_counts(unit_key_pattern) + return get_object_tag_counts(unit_key_pattern, count_implicit=True) def _was_xblock_ever_exam_linked_with_external(course, xblock): diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/xblock_helpers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/xblock_helpers.py index 3205b79a8887..7e589405a1c2 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/xblock_helpers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/xblock_helpers.py @@ -4,6 +4,7 @@ from opaque_keys.edx.keys import UsageKey from xmodule.modulestore.django import modulestore +from openedx.core.djangoapps.content_tagging.api import get_object_tag_counts def usage_key_with_run(usage_key_string): @@ -13,3 +14,13 @@ def usage_key_with_run(usage_key_string): usage_key = UsageKey.from_string(usage_key_string) usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) return usage_key + + +def get_children_tags_count(xblock): + """ + Returns a map with tag count of each child + """ + children = xblock.get_children() + child_usage_keys = [str(child.location) for child in children] + tags_count_query = ','.join(child_usage_keys) + return get_object_tag_counts(tags_count_query, count_implicit=True) diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index e5a0ab6a5b8f..8e0d5d887eeb 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -146,7 +146,6 @@ def get_exclude_list_of_fields(cls, course_key): exclude_list.append('allow_anonymous') exclude_list.append('allow_anonymous_to_peers') exclude_list.append('discussion_topics') - return exclude_list @classmethod diff --git a/cms/envs/common.py b/cms/envs/common.py index 11afd870a016..4ded7a9cff34 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -115,7 +115,6 @@ ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL, # Blockstore - BLOCKSTORE_USE_BLOCKSTORE_APP_API, BUNDLE_ASSET_STORAGE_SETTINGS, # Methods to derive settings @@ -298,9 +297,6 @@ # Enable content libraries (modulestore) search functionality 'ENABLE_LIBRARY_INDEX': False, - # Enable content libraries (blockstore) indexing - 'ENABLE_CONTENT_LIBRARY_INDEX': False, - # .. toggle_name: FEATURES['ALLOW_COURSE_RERUNS'] # .. toggle_implementation: DjangoSetting # .. toggle_default: True @@ -497,6 +493,16 @@ # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MST-1348' 'ENABLE_INTEGRITY_SIGNATURE': False, + # .. toggle_name: FEATURES['ENABLE_LTI_PII_ACKNOWLEDGEMENT'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Enables the lti pii acknowledgement feature for a course + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2023-10 + # .. toggle_target_removal_date: None + # .. toggle_tickets: 'https://2u-internal.atlassian.net/browse/MST-2055' + 'ENABLE_LTI_PII_ACKNOWLEDGEMENT': False, + # .. toggle_name: MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW # .. toggle_implementation: DjangoSetting # .. toggle_default: False @@ -2497,6 +2503,8 @@ CORS_ALLOW_INSECURE = False CORS_ALLOW_HEADERS = corsheaders_default_headers + ( 'use-jwt-cookie', + 'content-range', + 'content-disposition', ) LOGIN_REDIRECT_WHITELIST = [] @@ -2606,8 +2614,7 @@ PROCTORING_SETTINGS = {} ################## BLOCKSTORE RELATED SETTINGS ######################### -BLOCKSTORE_PUBLIC_URL_ROOT = 'http://localhost:18250' -BLOCKSTORE_API_URL = 'http://localhost:18250/api/v1/' + # Which of django's caches to use for storing anonymous user state for XBlocks # in the blockstore-based XBlock runtime XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE = 'default' diff --git a/cms/envs/devstack-experimental.yml b/cms/envs/devstack-experimental.yml index 8aa2c304af81..54f6edf8f9f2 100644 --- a/cms/envs/devstack-experimental.yml +++ b/cms/envs/devstack-experimental.yml @@ -40,8 +40,6 @@ AWS_SES_REGION_ENDPOINT: email.us-east-1.amazonaws.com AWS_SES_REGION_NAME: us-east-1 AWS_STORAGE_BUCKET_NAME: SET-ME-PLEASE (ex. bucket-name) BASE_COOKIE_DOMAIN: localhost -BLOCKSTORE_API_URL: http://localhost:18250/api/v1 -BLOCKSTORE_PUBLIC_URL_ROOT: http://localhost:18250 BLOCK_STRUCTURES_SETTINGS: COURSE_PUBLISH_TASK_DELAY: 30 TASK_DEFAULT_RETRY_DELAY: 30 diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 275a1fd31e72..74f5933822fe 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -147,7 +147,6 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing ################################ SEARCH INDEX ################################ FEATURES['ENABLE_COURSEWARE_INDEX'] = True FEATURES['ENABLE_LIBRARY_INDEX'] = False -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" ELASTIC_SEARCH_CONFIG = [ @@ -259,6 +258,8 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing CORS_ORIGIN_ALLOW_ALL = True CORS_ALLOW_HEADERS = corsheaders_default_headers + ( 'use-jwt-cookie', + 'content-range', + 'content-disposition', ) ################### Special Exams (Proctoring) and Prereqs ################### diff --git a/cms/envs/production.py b/cms/envs/production.py index d0a846d3da21..2ac80b94c39f 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -411,14 +411,6 @@ def get_env_setting(setting): # Configure an API auth token at (blockstore URL)/admin/authtoken/token/ BLOCKSTORE_API_AUTH_TOKEN = AUTH_TOKENS.get('BLOCKSTORE_API_AUTH_TOKEN', None) -# Datadog for events! -DATADOG = AUTH_TOKENS.get("DATADOG", {}) -DATADOG.update(ENV_TOKENS.get("DATADOG", {})) - -# TODO: deprecated (compatibility with previous settings) -if 'DATADOG_API' in AUTH_TOKENS: - DATADOG['api_key'] = AUTH_TOKENS['DATADOG_API'] - # Celery Broker CELERY_ALWAYS_EAGER = ENV_TOKENS.get("CELERY_ALWAYS_EAGER", False) CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "") @@ -510,7 +502,7 @@ def get_env_setting(setting): # Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='} VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {}) -if FEATURES['ENABLE_COURSEWARE_INDEX'] or FEATURES['ENABLE_LIBRARY_INDEX'] or FEATURES['ENABLE_CONTENT_LIBRARY_INDEX']: +if FEATURES['ENABLE_COURSEWARE_INDEX'] or FEATURES['ENABLE_LIBRARY_INDEX']: # Use ElasticSearch for the search engine SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" @@ -608,6 +600,8 @@ def get_env_setting(setting): CORS_ALLOW_INSECURE = ENV_TOKENS.get('CORS_ALLOW_INSECURE', False) CORS_ALLOW_HEADERS = corsheaders_default_headers + ( 'use-jwt-cookie', + 'content-range', + 'content-disposition', ) ################# Settings for brand logos. ################# diff --git a/cms/envs/test.py b/cms/envs/test.py index e7be26b73c0e..118d7e27a79c 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -27,8 +27,6 @@ # import settings from LMS for consistent behavior with CMS from lms.envs.test import ( # pylint: disable=wrong-import-order - BLOCKSTORE_USE_BLOCKSTORE_APP_API, - BLOCKSTORE_API_URL, COMPREHENSIVE_THEME_DIRS, # unimport:skip DEFAULT_FILE_STORAGE, ECOMMERCE_API_URL, @@ -184,10 +182,6 @@ } ############################### BLOCKSTORE ##################################### -# Blockstore tests -RUN_BLOCKSTORE_TESTS = os.environ.get('EDXAPP_RUN_BLOCKSTORE_TESTS', 'no').lower() in ('true', 'yes', '1') -BLOCKSTORE_API_URL = os.environ.get('EDXAPP_BLOCKSTORE_API_URL', "http://edx.devstack.blockstore-test:18251/api/v1/") -BLOCKSTORE_API_AUTH_TOKEN = os.environ.get('EDXAPP_BLOCKSTORE_API_AUTH_TOKEN', 'edxapp-test-key') BUNDLE_ASSET_STORAGE_SETTINGS = dict( STORAGE_CLASS='django.core.files.storage.FileSystemStorage', STORAGE_KWARGS=dict( @@ -265,21 +259,10 @@ # Courseware Search Index FEATURES['ENABLE_COURSEWARE_INDEX'] = True FEATURES['ENABLE_LIBRARY_INDEX'] = True -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = True -####################### ELASTICSEARCH TESTS ####################### -# Enable this when testing elasticsearch-based code which couldn't be tested using the mock engine -ENABLE_ELASTICSEARCH_FOR_TESTS = os.environ.get( - 'EDXAPP_ENABLE_ELASTICSEARCH_FOR_TESTS', 'no').lower() in ('true', 'yes', '1') - -TEST_ELASTICSEARCH_USE_SSL = os.environ.get( - 'EDXAPP_TEST_ELASTICSEARCH_USE_SSL', 'no').lower() in ('true', 'yes', '1') -TEST_ELASTICSEARCH_HOST = os.environ.get('EDXAPP_TEST_ELASTICSEARCH_HOST', 'edx.devstack.elasticsearch710') -TEST_ELASTICSEARCH_PORT = int(os.environ.get('EDXAPP_TEST_ELASTICSEARCH_PORT', '9200')) - ########################## AUTHOR PERMISSION ####################### FEATURES['ENABLE_CREATOR_GROUP'] = False diff --git a/cms/static/js/base.js b/cms/static/js/base.js index c8ab1a469145..5f970a89d592 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -75,6 +75,7 @@ function( $body.click(function() { $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); $('.nav-dd .nav-item .title').removeClass('is-selected'); + $('.custom-dropdown .dropdown-options').hide(); }); $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { diff --git a/cms/static/js/i18n/ar/djangojs.js b/cms/static/js/i18n/ar/djangojs.js index bdf5b60d33e3..ae1356486fc2 100644 --- a/cms/static/js/i18n/ar/djangojs.js +++ b/cms/static/js/i18n/ar/djangojs.js @@ -197,7 +197,6 @@ "API Secret": "\u0643\u0644\u0645\u0629 \u0633\u0631 \u0648\u0627\u062c\u0647\u0629 \u0628\u0631\u0645\u062c\u0629 \u0627\u0644\u062a\u0637\u0628\u064a\u0642 API Secret", "Abbreviation": "\u0627\u0644\u0627\u062e\u062a\u0635\u0627\u0631", "About Me": "\u0646\u0628\u0630\u0629 \u0639\u0646\u0651\u064a", - "About You": "\u0646\u0628\u0630\u0629 \u0639\u0646\u0643", "About me": "\u0646\u0628\u0630\u0629 \u0639\u0646\u064a", "Access to some content in this unit is restricted to specific groups of learners": "\u064a\u0642\u062a\u0635\u0631 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0628\u0639\u0636 \u0627\u0644\u0645\u062d\u062a\u0648\u064a\u0627\u062a \u0641\u064a \u0647\u0630\u0647 \u0627\u0644\u0648\u062d\u062f\u0629 \u0639\u0644\u0649 \u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0645\u062d\u062f\u062f\u0629 \u0645\u0646 \u0627\u0644\u0645\u062a\u0639\u0644\u0645\u064a\u0646", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u064a\u0642\u062a\u0635\u0631 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0628\u0639\u0636 \u0627\u0644\u0645\u062d\u062a\u0648\u0649 \u0641\u064a \u0647\u0630\u0627 {blockType} \u0639\u0644\u0649 \u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0645\u062d\u062f\u062f\u0629 \u0645\u0646 \u0627\u0644\u0645\u062a\u0639\u0644\u0645\u064a\u0646.", @@ -374,7 +373,6 @@ "Average": "\u0645\u062a\u0648\u0633\u0651\u0637", "Back to Full List": "\u0627\u0644\u0639\u0648\u062f\u0629 \u0625\u0644\u0649 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629", "Back to sign in": "\u0627\u0644\u0639\u0648\u062f\u0629 \u0625\u0644\u0649 \u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644", - "Back to {platform} FAQs": "\u0627\u0644\u0639\u0648\u062f\u0629 \u0625\u0644\u0649 \u0627\u0644\u0623\u0633\u0626\u0644\u0629 \u0627\u0644\u0634\u0627\u0626\u0639\u0629 \u0644\u0645\u0646\u0635\u0651\u0629 {platform} ", "Background color": "\u0644\u0648\u0646 \u0627\u0644\u062e\u0644\u0641\u064a\u0629", "Basic": "\u0623\u0633\u0627\u0633\u064a", "Basic Account Information": "\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0628 \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0629", @@ -1783,7 +1781,6 @@ "The following email addresses and/or usernames are invalid:": "\u0625\u0646\u0651 \u0639\u0646\u0627\u0648\u064a\u0646 \u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a \u0648/\u0623\u0648 \u0623\u0633\u0645\u0627\u0621 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0627\u0644\u062a\u0627\u0644\u064a\u0629 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629:", "The following errors were generated:": "\u0646\u062a\u062c\u062a \u0627\u0644\u0623\u062e\u0637\u0627\u0621 \u0627\u0644\u062a\u0627\u0644\u064a\u0629:", "The following file types are not allowed: ": "\u064a\u064f\u0645\u0646\u0639 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0623\u0646\u0648\u0627\u0639 \u0627\u0644\u0645\u0644\u0641\u0651\u0627\u062a \u0627\u0644\u062a\u0627\u0644\u064a\u0629:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "\u0627\u0644\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u062a\u0627\u0644\u064a\u0629 \u0647\u064a \u0628\u0627\u0644\u0641\u0639\u0644 \u062c\u0632\u0621 \u0645\u0646 \u0645\u0644\u0641\u0643 \u0627\u0644\u0634\u062e\u0635\u064a \u0639\u0644\u0649 {platform}. \u0644\u0642\u062f \u0623\u062f\u0631\u062c\u0646\u0627\u0647 \u0647\u0646\u0627 \u0644\u062a\u0633\u062c\u064a\u0644\u0643.", "The following message will be displayed at the bottom of the courseware pages within your course:": "\u0633\u062a\u064f\u0639\u0631\u064e\u0636 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629 \u0641\u064a \u0623\u0633\u0641\u0644 \u0635\u0641\u062d\u0627\u062a \u0645\u062d\u062a\u0648\u064a\u0627\u062a \u0645\u0633\u0627\u0642\u0643:", "The following options are available for the {license_name} license.": "\u062a\u062a\u0648\u0641\u0651\u0631 \u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062a\u0627\u0644\u064a\u0629 \u0644\u0644\u0625\u062c\u0627\u0632\u0629 {license_name}", "The following users are no longer enrolled in the course:": "\u0644\u0645 \u064a\u0639\u062f \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u0648\u0646 \u0627\u0644\u062a\u0627\u0644\u0648\u0646 \u0645\u0633\u062c\u0651\u0644\u064a\u0646 \u0628\u0627\u0644\u0645\u0633\u0627\u0642:", diff --git a/cms/static/js/i18n/ca/djangojs.js b/cms/static/js/i18n/ca/djangojs.js index de2ebca740ba..6cd6859b130c 100644 --- a/cms/static/js/i18n/ca/djangojs.js +++ b/cms/static/js/i18n/ca/djangojs.js @@ -80,7 +80,6 @@ "API Key": "Clau API", "API Secret": "API Secreta", "Abbreviation": "Abreviaci\u00f3", - "About You": "Sobre tu", "Access to some content in this unit is restricted to specific groups of learners": "L'acc\u00e9s a algun contingut d'aquesta unitat est\u00e0 restringit a grups espec\u00edfics d'estudiants", "Access to some content in this {blockType} is restricted to specific groups of learners.": "L'acc\u00e9s a algun contingut d'aquest {blockType} est\u00e0 restringit a grups espec\u00edfics d'estudiants.", "Access to this unit is restricted to: {selectedGroupsLabel}": "L'acc\u00e9s a aquesta unitat est\u00e0 restringit a: {selectedGroupsLabel}", @@ -164,7 +163,6 @@ "Automatic transcripts are disabled.": "Les transcripcions autom\u00e0tiques estan deshabilitades.", "Available %s": "%s Disponibles", "Back to sign in": "Torna a iniciar la sessi\u00f3", - "Back to {platform} FAQs": "Torna a preguntes m\u00e9s freq\u00fcents de {platform}", "Basic": "B\u00e0sic", "Be sure your entire face is inside the frame": "Assegureu-vos que tota la cara est\u00e0 dins del marc", "Before proceeding, please confirm that your details match": "Abans de continuar, confirmeu que les dades coincideixen", diff --git a/cms/static/js/i18n/ca@valencia/djangojs.js b/cms/static/js/i18n/ca@valencia/djangojs.js index 9daaef009f54..b0c279d5f33c 100644 --- a/cms/static/js/i18n/ca@valencia/djangojs.js +++ b/cms/static/js/i18n/ca@valencia/djangojs.js @@ -80,7 +80,6 @@ "API Key": "Clau API", "API Secret": "API Secreta", "Abbreviation": "Abreviaci\u00f3", - "About You": "Sobre tu", "Access to some content in this unit is restricted to specific groups of learners": "L'acc\u00e9s a algun contingut d'aquesta unitat est\u00e0 restringit a grups espec\u00edfics d'estudiants", "Access to some content in this {blockType} is restricted to specific groups of learners.": "L'acc\u00e9s a algun contingut d'aquest {blockType} est\u00e0 restringit a grups espec\u00edfics d'estudiants.", "Access to this unit is restricted to: {selectedGroupsLabel}": "L'acc\u00e9s a aquesta unitat est\u00e0 restringit a: {selectedGroupsLabel}", @@ -164,7 +163,6 @@ "Automatic transcripts are disabled.": "Les transcripcions autom\u00e0tiques estan deshabilitades.", "Available %s": "%s Disponibles", "Back to sign in": "Torna a iniciar la sessi\u00f3", - "Back to {platform} FAQs": "Torna a preguntes m\u00e9s freq\u00fcents de {platform}", "Basic": "B\u00e0sic", "Be sure your entire face is inside the frame": "Assegureu-vos que tota la cara est\u00e0 dins del marc", "Before proceeding, please confirm that your details match": "Abans de continuar, confirmeu que les dades coincideixen", diff --git a/cms/static/js/i18n/de-de/djangojs.js b/cms/static/js/i18n/de-de/djangojs.js index 9a6c5964f5f4..28ee241191ca 100644 --- a/cms/static/js/i18n/de-de/djangojs.js +++ b/cms/static/js/i18n/de-de/djangojs.js @@ -155,7 +155,6 @@ "API Secret": "API Geheimnis", "Abbreviation": "Abk\u00fcrzung", "About Me": "\u00dcber mich", - "About You": "\u00dcber Sie", "About me": "\u00dcber mich", "Access to some content in this unit is restricted to specific groups of learners": "Der Zugang zu einigen Inhalten in dieser Lektion ist auf bestimmte Gruppen von Lernenden beschr\u00e4nkt.", "Access to some content in this {blockType} is restricted to specific groups of learners.": "Der Zugang zu manchen Inhalten im {blockType} ist nur bestimmten Teilnehmergruppen erlaubt.", @@ -333,7 +332,6 @@ "Average": "Mittel", "Back to Full List": "Zur\u00fcck zur kompletten Liste", "Back to sign in": "Zur\u00fcck zum Einloggen", - "Back to {platform} FAQs": "Zur\u00fcck zu den {platform} FAQs", "Background color": "Hintergrundfarbe", "Basic": "Grundlegend", "Basic Account Information": "Profilinfo", @@ -358,7 +356,6 @@ "Border color": "Rahmenfarbe", "Bottom": "Grundlinie", "Browse": "Durchsuchen", - "Browse recently launched courses and see what's new in your favorite subjects.": "St\u00f6bern Sie in den k\u00fcrzlich hinzugef\u00fcgten Kursen und erfahren Sie, was es in Ihren Lieblingsthemen Neues gibt.", "Browse recently launched courses and see what\\'s new in your favorite subjects": "Neue Kurse durchsuchen", "Browsing": "Durchsuchen", "Bulk Exceptions": "Sammelausnahmen", @@ -794,7 +791,6 @@ "Explanation": "Erkl\u00e4rung", "Explicitly Hiding from Students": "Gezielt vor Teilnehmern verstecken", "Explore Programs": "Nach Programmen suchen", - "Explore courses": "Kurs\u00fcbersicht", "Explore your course!": "Erkunde ihr Kurs!", "Failed Proctoring": "Nicht bestanden unter Beaufsichtigung", "Failed to delete student state for user.": "Der Teilnehmerstatus f\u00fcr den Nutzer konnte nicht gel\u00f6scht werden.", @@ -1391,7 +1387,6 @@ "Reason for change:": "Grund f\u00fcr die \u00c4nderung:", "Receive updates": "Updates empfangen", "Recent Activity": "K\u00fcrzliche Aktivit\u00e4t", - "Recommendations for you": "Empfehlungen f\u00fcr Sie", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "Empfohlene Bildaufl\u00f6sung ist {imageResolution}, maximale Bilddateigr\u00f6\u00dfe sollte {maxFileSize} sein und das Format muss eines von {supportedImageFormats} sein.", "Recover my password": "Mein Passwort wiederherstellen", "Recovery Email Address": "E-Mail-Adresse f\u00fcr die Wiederherstellung", @@ -1733,7 +1728,6 @@ "The following email addresses and/or usernames are invalid:": "Die folgenden E-Mail-Adressen und/oder Nutzernamen sind ung\u00fcltig:", "The following errors were generated:": "Folgende Fehler wurden generiert:", "The following file types are not allowed: ": "Die folgenden Dateitypen sind nicht erlaubt:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "Die folgende Information ist Teil Ihres {platform} Profils. Wir haben es hier f\u00fcr Sie eingef\u00fcgt.", "The following message will be displayed at the bottom of the courseware pages within your course:": "Die folgende Nachricht wird im unteren Bereich Ihrers Kurses angezeigt. ", "The following options are available for the {license_name} license.": "Die folgenden Optionen sind f\u00fcr die {license_name} Lizenz verf\u00fcgbar.", "The following users are no longer enrolled in the course:": "Die folgenden Nutzer sind nicht l\u00e4nger in den Kurs eingeschrieben:", @@ -2320,7 +2314,6 @@ "internally reviewed": "Intern begutachtet / reviewed", "last activity": "letzte Aktivit\u00e4t", "less than a minute": "weniger als eine Minute", - "loading": "Wird geladen", "marked as answer %(time_ago)s": "als Antwort markiert, %(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "als Antwort markiert, %(time_ago)s von %(user)s", "minute": "Minute", diff --git a/cms/static/js/i18n/el/djangojs.js b/cms/static/js/i18n/el/djangojs.js index b285dc7c78cf..9558e34cf303 100644 --- a/cms/static/js/i18n/el/djangojs.js +++ b/cms/static/js/i18n/el/djangojs.js @@ -74,7 +74,9 @@ "An unexpected error occurred. Please try again.": "\u03a0\u03c1\u03bf\u03ad\u03ba\u03c5\u03c8\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "April": "\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2", "Are you sure you want to delete this post?": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03c2 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03c1\u03c4\u03b7\u03c3\u03b7;", - "Are you sure you want to delete this response?": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03c2 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7;", + "Are you sure you want to delete this response?": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03c2 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7;", + "Are you sure you want to unenroll from the verified {certNameLong} track of {courseName} ({courseNumber})?": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bc\u03ac\u03b8\u03b7\u03bc\u03b1 {certNameLong} {courseName} ({courseNumber});", + "Are you sure you want to unenroll from the verified {certNameLong} track of {courseName} ({courseNumber})?": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bc\u03ac\u03b8\u03b7\u03bc\u03b1 {certNameLong} {courseName} ({courseNumber});", "Are you sure you want to unenroll from {courseName} ({courseNumber})?": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bc\u03ac\u03b8\u03b7\u03bc\u03b1 {courseName} ({courseNumber});", "Assessment": "\u0391\u03be\u03b9\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7", "Assessments": "\u0391\u03be\u03b9\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03b5\u03b9\u03c2", @@ -126,6 +128,7 @@ "Course Name": "\u03a4\u03af\u03c4\u03bb\u03bf\u03c2 \u03bc\u03b1\u03b8\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "Course Number": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u039c\u03b1\u03b8\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "Create Account": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03b4\u03ce \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c3\u03b1\u03c2", + "Create a report of problem responses": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2", "Create an Account": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", "Create an Account.": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", "Create an account": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", @@ -153,8 +156,8 @@ "Engage with posts": "\u0394\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03b1\u03bd\u03b1\u03c1\u03c4\u03ae\u03c3\u03b5\u03c9\u03bd", "Enter Due Date and Time": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b1\u03c4\u03b5 \u03b7\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1 \u03ba\u03b1\u03b9 \u03ce\u03c1\u03b1 \u03bb\u03ae\u03be\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b8\u03b5\u03c3\u03bc\u03af\u03b1\u03c2 \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae\u03c2", "Enter a student's username or email address.": "\u03a0\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf \u03ae \u03c4\u03bf e-mail \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf\u03c5 \u03c3\u03c0\u03bf\u03c5\u03b4\u03b1\u03c3\u03c4\u03ae..", - "Enter a username or email.": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf \u03ae e-mail", - "Enter and confirm your new password.": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bd\u03ad\u03bf \u03c3\u03b1\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc.", + "Enter a username or email.": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf \u03ae e-mail", + "Enter and confirm your new password.": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bd\u03ad\u03bf \u03c3\u03b1\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc.", "Enter username or email": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf \u03ae \u03c4\u03bf e-mail \u03c3\u03b1\u03c2", "Enter your {platform_display_name} username or the URL to your {platform_display_name} page. Delete the URL to remove the link.": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf {platform_display_name} \u03ae \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf {platform_display_name}. \u0394\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf.", "Error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1", @@ -174,7 +177,8 @@ "Full Name": "\u039f\u03bd\u03bf\u03bc\u03b1\u03c4\u03b5\u03c0\u03ce\u03bd\u03c5\u03bc\u03bf", "Full Profile": "\u03a4\u03b7\u03bd \u03c0\u03bb\u03ae\u03c1\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae \u03bc\u03bf\u03c5 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1", "Gender": "\u03a6\u03cd\u03bb\u03bf", - "Grading": "\u0392\u0391\u0398\u039c\u039f\u039b\u039f\u0393\u0399\u0391", + "Generate Exception Certificates": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03b5\u03c9\u03bd", + "Grading": "\u0392\u03b1\u03b8\u03bc\u03bf\u03bb\u03bf\u03b3\u03af\u03b1", "Heading": "\u0395\u03c0\u03b9\u03ba\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b1", "Heading (Ctrl+H)": "\u0395\u03c0\u03b9\u03ba\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b1 (Ctrl+H)", "Hide": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7", @@ -235,7 +239,7 @@ "Password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2", "Password assistance": "\u039e\u03b5\u03c7\u03ac\u03c3\u03b1\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2;", "Password is incorrect": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", - "Passwords do not match.": "\u039f\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03b4\u03b5\u03bd \u03c3\u03c5\u03bc\u03c0\u03af\u03c0\u03c4\u03bf\u03c5\u03bd", + "Passwords do not match.": "\u039f\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd.", "Peer": "\u03a3\u03c5\u03bc\u03c6\u03bf\u03b9\u03c4\u03b7\u03c4\u03ae\u03c2", "Photo of %(fullName)s's ID": "\u03a6\u03c9\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03af\u03b1 \u03c4\u03bf\u03c5 %(fullName)s", "Please address the errors on this page first, and then save your progress.": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b4\u03b9\u03b5\u03c5\u03b8\u03b5\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03ce\u03c4\u03b1 \u03c4\u03b1 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03b1 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2.", @@ -310,9 +314,11 @@ "Student": "\u03a3\u03c0\u03bf\u03c5\u03b4\u03b1\u03c3\u03c4\u03ae\u03c2", "Submit": "\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae", "Successfully enrolled and sent email to the following users:": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03b4\u03ae\u03bb\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03c3\u03c4\u03bf\u03c5\u03c2 \u03b5\u03be\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2:", + "Successfully started task to reset attempts for problem '<%- problem_id %>'. Click the 'Show Task Status' button to see the status of the task.": "\u039e\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5 \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1 \u03b7 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03b9\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c4\u03b5\u03c3\u03c4 \"<%- problem_id %>\". \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2\", \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2.", "Support education research by providing additional information": "\u03a3\u03c5\u03bd\u03b5\u03b9\u03c3\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03ba\u03c0\u03b1\u03b9\u03b4\u03b5\u03c5\u03c4\u03b9\u03ba\u03ae \u03ad\u03c1\u03b5\u03c5\u03bd\u03b1 \u03b4\u03af\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 \u03ba\u03ac\u03c0\u03bf\u03b9\u03b5\u03c2 \u03b5\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "Supported file types: {supportedVideoTypes}": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03bf\u03af \u03c4\u03cd\u03c0\u03bf\u03b9 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd: {supportedVideoTypes}", "TOTAL": "\u03a3\u03a5\u039d\u039f\u039b\u039f", + "Task Status": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2", "Team Description (Required) *": "\u03a0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u039f\u03bc\u03ac\u03b4\u03b1\u03c2 (\u03a5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03b4\u03af\u03bf)*", "Team Name (Required) *": "\u038c\u03bd\u03bf\u03bc\u03b1 \u039f\u03bc\u03ac\u03b4\u03b1\u03c2 (\u03a5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03b4\u03af\u03bf)*", "Terms of Service and Honor Code": "\u038c\u03c1\u03bf\u03b9 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u039a\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 \u03a4\u03b9\u03bc\u03ae\u03c2", @@ -356,7 +362,7 @@ "URL": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", "Undo (Ctrl+Z)": "\u0391\u03bd\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 (Ctrl+Z)", "Undo Changes": "\u0391\u03bd\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ce\u03bd", - "Unendorse": "\u0394\u03b5\u03bd \u03c0\u03c1\u03bf\u03c4\u03b5\u03af\u03bd\u03c9", + "Unendorse": "\u0391\u03bd\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2 \"\u03a0\u03c1\u03bf\u03c4\u03b5\u03af\u03bd\u03c9\"", "Unit": "\u039a\u03b5\u03c6\u03ac\u03bb\u03b1\u03b9\u03bf", "Upload File": "\u039c\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u0391\u03c1\u03c7\u03b5\u03af\u03bf\u03c5", "Upload New File": "\u039c\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03bd\u03ad\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5", @@ -380,7 +386,7 @@ "We couldn't sign you in.": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae", "We've sent a confirmation message to {new_email_address}. Click the link in the message to update your email address.": "\u03a3\u03b1\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bc\u03b5 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c3\u03c4\u03bf \u03bd\u03ad\u03bf e-mail {new_email_address} \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03c3\u03b1\u03c4\u03b5. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03bf e-mail \u03c3\u03b1\u03c2.", "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {anchorStart}technical support{anchorEnd}.": "\u0388\u03c7\u03bf\u03c5\u03bc\u03b5 \u03b1\u03c0\u03bf\u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {email}. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03bb\u03ac\u03b2\u03b1\u03c4\u03b5 \u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03b7\u03bd {anchorStart}\u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7{anchorEnd}.", - "We\u2019re sorry to see you go!": "\u039b\u03c5\u03c0\u03bf\u03cd\u03bc\u03b1\u03c3\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c7\u03ce\u03c1\u03b7\u03c3\u03ae \u03c3\u03b1\u03c2!", + "We\u2019re sorry to see you go!": "\u039b\u03c5\u03c0\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c7\u03ce\u03c1\u03b7\u03c3\u03ae \u03c3\u03b1\u03c2!", "Year of Birth": "\u0388\u03c4\u03bf\u03c2 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2", "Yesterday": "\u03a7\u03b8\u03ad\u03c2", "You are currently sharing a limited profile.": "\u039f\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ad\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03ba\u03bf\u03b9\u03bd\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b4\u03b7\u03bc\u03cc\u03c3\u03b9\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2", diff --git a/cms/static/js/i18n/eo/djangojs.js b/cms/static/js/i18n/eo/djangojs.js index 3a995b184a46..906831334906 100644 --- a/cms/static/js/i18n/eo/djangojs.js +++ b/cms/static/js/i18n/eo/djangojs.js @@ -126,7 +126,6 @@ "API Secret": "\u00c0P\u00cc S\u00e9\u00e7r\u00e9t \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3#", "Abbreviation": "\u00c0\u00df\u00dfr\u00e9v\u00ef\u00e4t\u00ef\u00f6n \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", "About Me": "\u00c0\u00df\u00f6\u00fct M\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202#", - "About You": "\u00c0\u00df\u00f6\u00fct \u00dd\u00f6\u00fc \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142#", "About me": "\u00c0\u00df\u00f6\u00fct m\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202#", "Access to some content in this unit is restricted to specific groups of learners": "\u00c0\u00e7\u00e7\u00e9ss t\u00f6 s\u00f6m\u00e9 \u00e7\u00f6nt\u00e9nt \u00efn th\u00efs \u00fcn\u00eft \u00efs r\u00e9str\u00ef\u00e7t\u00e9d t\u00f6 sp\u00e9\u00e7\u00eff\u00ef\u00e7 gr\u00f6\u00fcps \u00f6f l\u00e9\u00e4rn\u00e9rs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454#", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u00c0\u00e7\u00e7\u00e9ss t\u00f6 s\u00f6m\u00e9 \u00e7\u00f6nt\u00e9nt \u00efn th\u00efs {blockType} \u00efs r\u00e9str\u00ef\u00e7t\u00e9d t\u00f6 sp\u00e9\u00e7\u00eff\u00ef\u00e7 gr\u00f6\u00fcps \u00f6f l\u00e9\u00e4rn\u00e9rs. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454#", @@ -259,6 +258,7 @@ "Any course progress or grades from your current session will be lost.": "\u00c0n\u00fd \u00e7\u00f6\u00fcrs\u00e9 pr\u00f6gr\u00e9ss \u00f6r gr\u00e4d\u00e9s fr\u00f6m \u00fd\u00f6\u00fcr \u00e7\u00fcrr\u00e9nt s\u00e9ss\u00ef\u00f6n w\u00efll \u00df\u00e9 l\u00f6st. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "Any divided discussion topics are divided based on cohort.": "\u00c0n\u00fd d\u00efv\u00efd\u00e9d d\u00efs\u00e7\u00fcss\u00ef\u00f6n t\u00f6p\u00ef\u00e7s \u00e4r\u00e9 d\u00efv\u00efd\u00e9d \u00df\u00e4s\u00e9d \u00f6n \u00e7\u00f6h\u00f6rt. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", "Any divided discussion topics are divided based on enrollment track.": "\u00c0n\u00fd d\u00efv\u00efd\u00e9d d\u00efs\u00e7\u00fcss\u00ef\u00f6n t\u00f6p\u00ef\u00e7s \u00e4r\u00e9 d\u00efv\u00efd\u00e9d \u00df\u00e4s\u00e9d \u00f6n \u00e9nr\u00f6llm\u00e9nt tr\u00e4\u00e7k. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", + "Application Details": "\u00c0ppl\u00ef\u00e7\u00e4t\u00ef\u00f6n D\u00e9t\u00e4\u00efls \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442,#", "Approved in Another Course": "\u00c0ppr\u00f6v\u00e9d \u00efn \u00c0n\u00f6th\u00e9r \u00c7\u00f6\u00fcrs\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455#", "April": "aprilo", "Are you having trouble finding a team to join?": "\u00c0r\u00e9 \u00fd\u00f6\u00fc h\u00e4v\u00efng tr\u00f6\u00fc\u00dfl\u00e9 f\u00efnd\u00efng \u00e4 t\u00e9\u00e4m t\u00f6 j\u00f6\u00efn? \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", @@ -309,7 +309,6 @@ "Average": "\u00c0v\u00e9r\u00e4g\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c #", "Back to Full List": "B\u00e4\u00e7k t\u00f6 F\u00fcll L\u00efst \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454#", "Back to sign in": "B\u00e4\u00e7k t\u00f6 s\u00efgn \u00efn \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", - "Back to {platform} FAQs": "B\u00e4\u00e7k t\u00f6 {platform} F\u00c0Qs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c#", "Background color": "B\u00e4\u00e7kgr\u00f6\u00fcnd \u00e7\u00f6l\u00f6r \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c#", "Basic": "B\u00e4s\u00ef\u00e7 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455#", "Basic Account Information": "B\u00e4s\u00ef\u00e7 \u00c0\u00e7\u00e7\u00f6\u00fcnt \u00ccnf\u00f6rm\u00e4t\u00ef\u00f6n \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455#", @@ -334,7 +333,6 @@ "Border color": "B\u00f6rd\u00e9r \u00e7\u00f6l\u00f6r \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", "Bottom": "B\u00f6tt\u00f6m \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5#", "Browse": "Br\u00f6ws\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5#", - "Browse recently launched courses and see what's new in your favorite subjects.": "Br\u00f6ws\u00e9 r\u00e9\u00e7\u00e9ntl\u00fd l\u00e4\u00fcn\u00e7h\u00e9d \u00e7\u00f6\u00fcrs\u00e9s \u00e4nd s\u00e9\u00e9 wh\u00e4t's n\u00e9w \u00efn \u00fd\u00f6\u00fcr f\u00e4v\u00f6r\u00eft\u00e9 s\u00fc\u00dfj\u00e9\u00e7ts. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442#", "Browse recently launched courses and see what\\'s new in your favorite subjects": "Br\u00f6ws\u00e9 r\u00e9\u00e7\u00e9ntl\u00fd l\u00e4\u00fcn\u00e7h\u00e9d \u00e7\u00f6\u00fcrs\u00e9s \u00e4nd s\u00e9\u00e9 wh\u00e4t\\'s n\u00e9w \u00efn \u00fd\u00f6\u00fcr f\u00e4v\u00f6r\u00eft\u00e9 s\u00fc\u00dfj\u00e9\u00e7ts \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442#", "Browsing": "Br\u00f6ws\u00efng \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202#", "Bulk Exceptions": "B\u00fclk \u00c9x\u00e7\u00e9pt\u00ef\u00f6ns \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", @@ -479,6 +477,7 @@ "Copy": "\u00c7\u00f6p\u00fd \u2c60'\u03c3\u044f\u0454\u043c \u03b9#", "Copy Component Location": "\u00c7\u00f6p\u00fd \u00c7\u00f6mp\u00f6n\u00e9nt L\u00f6\u00e7\u00e4t\u00ef\u00f6n \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3#", "Copy Email To Editor": "\u00c7\u00f6p\u00fd \u00c9m\u00e4\u00efl T\u00f6 \u00c9d\u00eft\u00f6r \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", + "Copy Unit": "\u00c7\u00f6p\u00fd \u00dbn\u00eft \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142#", "Copy of '{componentDisplayName}'": "\u00c7\u00f6p\u00fd \u00f6f '{componentDisplayName}' \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9#", "Copy row": "\u00c7\u00f6p\u00fd r\u00f6w \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202#", "Copy to Clipboard": "\u00c7\u00f6p\u00fd t\u00f6 \u00c7l\u00efp\u00df\u00f6\u00e4rd \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454#", @@ -790,7 +789,6 @@ "Explanation": "\u00c9xpl\u00e4n\u00e4t\u00ef\u00f6n \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f #", "Explicitly Hiding from Students": "\u00c9xpl\u00ef\u00e7\u00eftl\u00fd H\u00efd\u00efng fr\u00f6m St\u00fcd\u00e9nts \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442#", "Explore Programs": "\u00c9xpl\u00f6r\u00e9 Pr\u00f6gr\u00e4ms \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c#", - "Explore courses": "\u00c9xpl\u00f6r\u00e9 \u00e7\u00f6\u00fcrs\u00e9s \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", "Explore new programs": "\u00c9xpl\u00f6r\u00e9 n\u00e9w pr\u00f6gr\u00e4ms \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", "Explore subscription options": "\u00c9xpl\u00f6r\u00e9 s\u00fc\u00dfs\u00e7r\u00efpt\u00ef\u00f6n \u00f6pt\u00ef\u00f6ns \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2#", "Explore your course!": "\u00c9xpl\u00f6r\u00e9 \u00fd\u00f6\u00fcr \u00e7\u00f6\u00fcrs\u00e9! \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", @@ -1105,6 +1103,7 @@ "Manage Learners": "M\u00e4n\u00e4g\u00e9 L\u00e9\u00e4rn\u00e9rs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", "Manage Tags": "M\u00e4n\u00e4g\u00e9 T\u00e4gs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f #", "Manage my subscription": "M\u00e4n\u00e4g\u00e9 m\u00fd s\u00fc\u00dfs\u00e7r\u00efpt\u00ef\u00f6n \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2#", + "Manage tags": "M\u00e4n\u00e4g\u00e9 t\u00e4gs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f #", "Manual": "M\u00e4n\u00fc\u00e4l \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5#", "March": "marto", "Mark Exam As Completed": "M\u00e4rk \u00c9x\u00e4m \u00c0s \u00c7\u00f6mpl\u00e9t\u00e9d \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2#", @@ -1394,6 +1393,7 @@ "Professional Education Verified Certificate": "Pr\u00f6f\u00e9ss\u00ef\u00f6n\u00e4l \u00c9d\u00fc\u00e7\u00e4t\u00ef\u00f6n V\u00e9r\u00eff\u00ef\u00e9d \u00c7\u00e9rt\u00eff\u00ef\u00e7\u00e4t\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "Profile": "Pr\u00f6f\u00efl\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c #", "Profile Image": "Pr\u00f6f\u00efl\u00e9 \u00ccm\u00e4g\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9#", + "Profile Information": "Pr\u00f6f\u00efl\u00e9 \u00ccnf\u00f6rm\u00e4t\u00ef\u00f6n \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442,#", "Profile Visibility:": "Pr\u00f6f\u00efl\u00e9 V\u00efs\u00ef\u00df\u00efl\u00eft\u00fd: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442,#", "Profile image for {username}": "Pr\u00f6f\u00efl\u00e9 \u00efm\u00e4g\u00e9 f\u00f6r {username} \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", "Program Record": "Pr\u00f6gr\u00e4m R\u00e9\u00e7\u00f6rd \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442#", @@ -1422,7 +1422,6 @@ "Reason for change:": "R\u00e9\u00e4s\u00f6n f\u00f6r \u00e7h\u00e4ng\u00e9: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442#", "Receive updates": "R\u00e9\u00e7\u00e9\u00efv\u00e9 \u00fcpd\u00e4t\u00e9s \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", "Recent Activity": "R\u00e9\u00e7\u00e9nt \u00c0\u00e7t\u00efv\u00eft\u00fd \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", - "Recommendations for you": "R\u00e9\u00e7\u00f6mm\u00e9nd\u00e4t\u00ef\u00f6ns f\u00f6r \u00fd\u00f6\u00fc \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3#", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "R\u00e9\u00e7\u00f6mm\u00e9nd\u00e9d \u00efm\u00e4g\u00e9 r\u00e9s\u00f6l\u00fct\u00ef\u00f6n \u00efs {imageResolution}, m\u00e4x\u00efm\u00fcm \u00efm\u00e4g\u00e9 f\u00efl\u00e9 s\u00efz\u00e9 sh\u00f6\u00fcld \u00df\u00e9 {maxFileSize} \u00e4nd f\u00f6rm\u00e4t m\u00fcst \u00df\u00e9 \u00f6n\u00e9 \u00f6f {supportedImageFormats}. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 #", "Recover my password": "R\u00e9\u00e7\u00f6v\u00e9r m\u00fd p\u00e4ssw\u00f6rd \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442,#", "Recovery Email Address": "R\u00e9\u00e7\u00f6v\u00e9r\u00fd \u00c9m\u00e4\u00efl \u00c0ddr\u00e9ss \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2#", @@ -1791,7 +1790,7 @@ "The following errors were generated:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng \u00e9rr\u00f6rs w\u00e9r\u00e9 g\u00e9n\u00e9r\u00e4t\u00e9d: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5#", "The following file types are not allowed: ": "Th\u00e9 f\u00f6ll\u00f6w\u00efng f\u00efl\u00e9 t\u00fdp\u00e9s \u00e4r\u00e9 n\u00f6t \u00e4ll\u00f6w\u00e9d: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "The following files already exist in this course but don't match the version used by the component you pasted:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng f\u00efl\u00e9s \u00e4lr\u00e9\u00e4d\u00fd \u00e9x\u00efst \u00efn th\u00efs \u00e7\u00f6\u00fcrs\u00e9 \u00df\u00fct d\u00f6n't m\u00e4t\u00e7h th\u00e9 v\u00e9rs\u00ef\u00f6n \u00fcs\u00e9d \u00df\u00fd th\u00e9 \u00e7\u00f6mp\u00f6n\u00e9nt \u00fd\u00f6\u00fc p\u00e4st\u00e9d: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "Th\u00e9 f\u00f6ll\u00f6w\u00efng \u00efnf\u00f6rm\u00e4t\u00ef\u00f6n \u00efs \u00e4lr\u00e9\u00e4d\u00fd \u00e4 p\u00e4rt \u00f6f \u00fd\u00f6\u00fcr {platform} pr\u00f6f\u00efl\u00e9. W\u00e9'v\u00e9 \u00efn\u00e7l\u00fcd\u00e9d \u00eft h\u00e9r\u00e9 f\u00f6r \u00fd\u00f6\u00fcr \u00e4ppl\u00ef\u00e7\u00e4t\u00ef\u00f6n. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", + "The following information is already a part of your {platform} profile and is required for your application. To edit this information go to ": "Th\u00e9 f\u00f6ll\u00f6w\u00efng \u00efnf\u00f6rm\u00e4t\u00ef\u00f6n \u00efs \u00e4lr\u00e9\u00e4d\u00fd \u00e4 p\u00e4rt \u00f6f \u00fd\u00f6\u00fcr {platform} pr\u00f6f\u00efl\u00e9 \u00e4nd \u00efs r\u00e9q\u00fc\u00efr\u00e9d f\u00f6r \u00fd\u00f6\u00fcr \u00e4ppl\u00ef\u00e7\u00e4t\u00ef\u00f6n. T\u00f6 \u00e9d\u00eft th\u00efs \u00efnf\u00f6rm\u00e4t\u00ef\u00f6n g\u00f6 t\u00f6 #", "The following message will be displayed at the bottom of the courseware pages within your course:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng m\u00e9ss\u00e4g\u00e9 w\u00efll \u00df\u00e9 d\u00efspl\u00e4\u00fd\u00e9d \u00e4t th\u00e9 \u00df\u00f6tt\u00f6m \u00f6f th\u00e9 \u00e7\u00f6\u00fcrs\u00e9w\u00e4r\u00e9 p\u00e4g\u00e9s w\u00efth\u00efn \u00fd\u00f6\u00fcr \u00e7\u00f6\u00fcrs\u00e9: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", "The following options are available for the {license_name} license.": "Th\u00e9 f\u00f6ll\u00f6w\u00efng \u00f6pt\u00ef\u00f6ns \u00e4r\u00e9 \u00e4v\u00e4\u00efl\u00e4\u00dfl\u00e9 f\u00f6r th\u00e9 {license_name} l\u00ef\u00e7\u00e9ns\u00e9. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", "The following required files could not be added to the course:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng r\u00e9q\u00fc\u00efr\u00e9d f\u00efl\u00e9s \u00e7\u00f6\u00fcld n\u00f6t \u00df\u00e9 \u00e4dd\u00e9d t\u00f6 th\u00e9 \u00e7\u00f6\u00fcrs\u00e9: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", @@ -2426,7 +2425,6 @@ "incorrect": "\u00efn\u00e7\u00f6rr\u00e9\u00e7t \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142#", "last activity": "l\u00e4st \u00e4\u00e7t\u00efv\u00eft\u00fd \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9#", "less than a minute": "l\u00e9ss th\u00e4n \u00e4 m\u00efn\u00fct\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442#", - "loading": "l\u00f6\u00e4d\u00efng \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c #", "marked as answer %(time_ago)s": "m\u00e4rk\u00e9d \u00e4s \u00e4nsw\u00e9r %(time_ago)s \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", "marked as answer %(time_ago)s by %(user)s": "m\u00e4rk\u00e9d \u00e4s \u00e4nsw\u00e9r %(time_ago)s \u00df\u00fd %(user)s \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454#", "minute": "m\u00efn\u00fct\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5#", diff --git a/cms/static/js/i18n/es-419/djangojs.js b/cms/static/js/i18n/es-419/djangojs.js index 549e97e0b542..4d839331c597 100644 --- a/cms/static/js/i18n/es-419/djangojs.js +++ b/cms/static/js/i18n/es-419/djangojs.js @@ -242,7 +242,6 @@ "API Secret": "Secreto API", "Abbreviation": "Abreviatura", "About Me": "Sobre M\u00ed", - "About You": "Acerca de usted", "About me": "Sobre m\u00ed", "Access to some content in this unit is restricted to specific groups of learners": "Acceso a ciertos contenidos en esta unidad est\u00e1 restringido a grupos espec\u00edficos de estudiantes.", "Access to some content in this {blockType} is restricted to specific groups of learners.": "El acceso a algunos contenidos en este {blockType} est\u00e1 restringido a grupos espec\u00edficos de estudiantes.", @@ -376,6 +375,7 @@ "Any course progress or grades from your current session will be lost.": "Se perder\u00e1 cualquier calificaci\u00f3n o progreso del curso en la sesi\u00f3n actual. ", "Any divided discussion topics are divided based on cohort.": "La divisi\u00f3n de temas de discusi\u00f3n se basa en el cohorte.", "Any divided discussion topics are divided based on enrollment track.": "La divisi\u00f3n de temas de discusi\u00f3n se basa en la ruta de inscripci\u00f3n.", + "Application Details": "Detalles de la aplicaci\u00f3n", "April": "Abril", "Are you having trouble finding a team to join?": "\u00bfTiene problemas para encontrar un equipo al cual unirse?", "Are you sure that you want to leave this session?": "\u00bf Est\u00e1 seguro de que quiere salir de esta sesi\u00f3n?", @@ -422,7 +422,6 @@ "Average": "Normal", "Back to Full List": "Volver a la lista completa", "Back to sign in": "Volver al inicio", - "Back to {platform} FAQs": "Regresar a FAQs de {platform}", "Background color": "Color de fondo", "Basic": "B\u00e1sico", "Basic Account Information": "Informaci\u00f3n b\u00e1sica de la cuenta", @@ -447,7 +446,6 @@ "Border color": "Color de borde", "Bottom": "Base", "Browse": "Explorar", - "Browse recently launched courses and see what's new in your favorite subjects.": "Echa un vistazo a nuestros cursos lanzados recientemente y qu\u00e9 hay de nuevo en tus temas favoritos.", "Browse recently launched courses and see what\\'s new in your favorite subjects": "Echa un vistazo a nuestros cursos lanzados recientemente y qu\u00e9 hay de nuevo en tus temas favoritos.", "Browsing": "Explorando", "Bulk Exceptions": "Excepciones en lote", @@ -599,6 +597,7 @@ "Copy Component Location": "Copiar la ubicaci\u00f3n del Componente", "Copy Email To Editor": "Copiar el correo al editor", "Copy Exam Code": "Copia el C\u00f3digo de el Examen", + "Copy Unit": "Copiar unidad", "Copy of '{componentDisplayName}'": "Copia de '{componentDisplayName}'", "Copy row": "Copiar la fila", "Copy to Clipboard": "Copiar al portapapeles", @@ -910,7 +909,6 @@ "Explanation": "Explicaci\u00f3n", "Explicitly Hiding from Students": "Ocultar solo a los estudiantes", "Explore Programs": "Explorar programas", - "Explore courses": "Explorar cursos", "Explore new programs": "Explorar nuevos programas", "Explore subscription options": "Explorar las opciones de suscripci\u00f3n", "Explore your course!": "Explora tus cursos!", @@ -1232,6 +1230,7 @@ "Manage Learners": "Manejar Estudiantes", "Manage Tags": "Administrar etiquetas", "Manage my subscription": "Gestionar mi suscripci\u00f3n", + "Manage tags": "Administrar etiquetas", "Manual": "Manual", "March": "Marzo", "Mark Exam As Completed": "Marcar el examen como completado", @@ -1534,6 +1533,7 @@ "Professional Education Verified Certificate": "Certificado Verificado de Educaci\u00f3n Profesional", "Profile": "Perfil", "Profile Image": "Foto de perfil", + "Profile Information": "Informaci\u00f3n del perfil", "Profile Visibility:": "Visibilidad del perfil:", "Profile image for {username}": "Foto de perfil para {username}", "Program Record": "Registro del Programa", @@ -1562,7 +1562,6 @@ "Reason for change:": "Motivo del cambio:", "Receive updates": "Recibir notificaciones", "Recent Activity": "Actividad Reciente", - "Recommendations for you": "Recomendaciones para ti", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "La resoluci\u00f3n recomendada de la imagen es {imageResolution}, el m\u00e1ximo tama\u00f1o del archivo de la imagen deber\u00eda ser {maxFileSize} y el formato debe ser uno de {supportedImageFormats}.", "Recover my password": "Recuperar mi contrase\u00f1a", "Recovery Email Address": "Direcci\u00f3n de correo electr\u00f3nico de recuperaci\u00f3n", @@ -1935,7 +1934,7 @@ "The following errors were generated:": "Se generaron los siguientes errores:", "The following file types are not allowed: ": "Los siguientes tipos de archivos son soportados:", "The following files already exist in this course but don't match the version used by the component you pasted:": "Los siguientes archivos ya existen en este curso, pero no coinciden con la versi\u00f3n utilizada por el componente que peg\u00f3:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "La siguiente informaci\u00f3n ya es parte de su perfil en {platform} . La hemos incluido aqu\u00ed para su aplicaci\u00f3n", + "The following information is already a part of your {platform} profile and is required for your application. To edit this information go to ": "La siguiente informaci\u00f3n ya forma parte de su perfil {platform} y es necesaria para su solicitud. Para editar esta informaci\u00f3n vaya a", "The following message will be displayed at the bottom of the courseware pages within your course:": "El siguiente mensaje ser\u00e1 mostrado al final de las p\u00e1ginas de los cursos. ", "The following options are available for the {license_name} license.": "Las siguientes opciones est\u00e1n disponibles para {license_name} licencia", "The following required files could not be added to the course:": "Los siguientes archivos obligatorios no se pudieron agregar al curso:", @@ -2587,7 +2586,6 @@ "internally reviewed": "revisado internamente", "last activity": "\u00faltima actividad", "less than a minute": "menos de un minuto", - "loading": "cargando", "marked as answer %(time_ago)s": "marcado como respuesta hace %(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "marcado como respuesta hace %(time_ago)s por %(user)s", "minute": "minuto", diff --git a/cms/static/js/i18n/eu-es/djangojs.js b/cms/static/js/i18n/eu-es/djangojs.js index 1ad6e260549b..6ab20349894b 100644 --- a/cms/static/js/i18n/eu-es/djangojs.js +++ b/cms/static/js/i18n/eu-es/djangojs.js @@ -84,7 +84,6 @@ "API Key": "API gakoa", "Abbreviation": "Laburdura", "About Me": "Niri buruz", - "About You": "Zuri buruz", "About me": "Niri buruz", "Account": "Kontua", "Account Information": "Kontuaren informazioa", @@ -190,7 +189,6 @@ "Average": "Batez bestekoa", "Back to Full List": "Itzuli zerrenda osora", "Back to sign in": "Itzuli saioa hasteko orrira", - "Back to {platform} FAQs": "Itzuli {platform} plataformako FAQetara", "Background color": "Atzeko planoaren kolorea", "Basic": "Oinarrizkoa", "Basic Account Information": "Kontuaren oinarrizko informazioa", diff --git a/cms/static/js/i18n/fa-ir/djangojs.js b/cms/static/js/i18n/fa-ir/djangojs.js index 6714c1afaec4..f688b97f0e87 100644 --- a/cms/static/js/i18n/fa-ir/djangojs.js +++ b/cms/static/js/i18n/fa-ir/djangojs.js @@ -242,7 +242,6 @@ "API Secret": "API Secret", "Abbreviation": "\u0633\u0631\u0646\u0627\u0645", "About Me": "\u062f\u0631\u0628\u0627\u0631\u0647 \u0645\u0646", - "About You": "\u062f\u0631\u0628\u0627\u0631\u06c0 \u062a\u0648", "About me": "\u062f\u0631\u0628\u0627\u0631\u06c0 \u0645\u0646", "Access to some content in this unit is restricted to specific groups of learners": "\u062f\u0633\u062a\u0631\u0633\u06cc \u0628\u0647 \u0627\u06cc\u0646 \u0648\u0627\u062d\u062f \u0645\u062d\u062f\u0648\u062f \u0628\u0647 \u06af\u0631\u0648\u0647\u200c\u0647\u0627\u06cc \u0648\u06cc\u0698\u0647\u200c\u0627\u06cc \u0627\u0632 \u06cc\u0627\u062f\u06af\u06cc\u0631\u0646\u062f\u06af\u0627\u0646 \u0627\u0633\u062a", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u062f\u0633\u062a\u0631\u0633\u06cc \u0628\u0647 \u0627\u06cc\u0646 {blockType} \u0645\u062d\u062f\u0648\u062f \u0628\u0647 \u06af\u0631\u0648\u0647 \u0648\u06cc\u0698\u0647\u200c\u0627\u06cc \u0627\u0632 \u06cc\u0627\u062f\u06af\u06cc\u0631\u0646\u062f\u06af\u0627\u0646 \u0627\u0633\u062a .", @@ -422,7 +421,6 @@ "Average": "\u0645\u06cc\u0627\u0646\u06af\u06cc\u0646", "Back to Full List": "\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u0641\u0647\u0631\u0633\u062a \u06a9\u0627\u0645\u0644", "Back to sign in": "\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u0642\u0633\u0645\u062a \u0648\u0631\u0648\u062f \u0628\u0647 \u0633\u0627\u0645\u0627\u0646\u0647", - "Back to {platform} FAQs": "\u0628\u0631\u06af\u0631\u062f \u0628\u0647 \u0633\u0624\u0627\u0644\u0627\u062a \u067e\u0631\u062a\u06a9\u0631\u0627\u0631 {platform}", "Background color": "\u0631\u0646\u06af \u067e\u0633\u200c\u0632\u0645\u06cc\u0646\u0647", "Basic": "\u0627\u0628\u062a\u062f\u0627\u06cc\u06cc", "Basic Account Information": "\u0627\u0637\u0644\u0627\u0639\u0627\u062a \u06a9\u0627\u0631\u0628\u0631\u06cc \u0627\u0648\u0644\u06cc\u0647", @@ -447,7 +445,6 @@ "Border color": "\u0631\u0646\u06af \u062d\u0627\u0634\u06cc\u0647", "Bottom": "\u067e\u0627\u06cc\u06cc\u0646", "Browse": "\u062a\u0648\u0631\u0642", - "Browse recently launched courses and see what's new in your favorite subjects.": "\u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc\u06cc \u0631\u0627 \u06a9\u0647 \u0627\u062e\u06cc\u0631\u0627\u064b \u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc \u0634\u062f\u0647\u200c\u0627\u0646\u062f \u0645\u0631\u0648\u0631 \u06a9\u0646\u06cc\u062f \u0648 \u0628\u0628\u06cc\u0646\u06cc\u062f \u0686\u0647 \u0645\u0648\u0627\u0631\u062f \u062c\u062f\u06cc\u062f\u06cc \u062f\u0631 \u0645\u0648\u0636\u0648\u0639\u0627\u062a \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u06c0 \u0634\u0645\u0627 \u0648\u062c\u0648\u062f \u062f\u0627\u0631\u062f.", "Browse recently launched courses and see what\\'s new in your favorite subjects": "\u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc\u06cc \u06a9\u0647 \u0627\u062e\u06cc\u0631\u0627\u064b \u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc\u200c\u0634\u062f\u0647\u200c \u0631\u0627 \u0645\u0631\u0648\u0631 \u06a9\u0631\u062f\u0647 \u0648 \u0645\u0648\u0636\u0648\u0639\u0627\u062a \u062c\u062f\u06cc\u062f \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647 \u062e\u0648\u062f \u0631\u0627 \u0628\u0628\u06cc\u0646\u06cc\u062f", "Browsing": "\u062a\u0648\u0631\u0642", "Bulk Exceptions": "\u0627\u0633\u062a\u062b\u0646\u0627\u0626\u0627\u062a \u0627\u0646\u0628\u0648\u0647", @@ -909,7 +906,6 @@ "Explanation": "\u062a\u0648\u0636\u06cc\u062d", "Explicitly Hiding from Students": "\u0635\u0631\u06cc\u062d\u0627 \u0627\u0632 \u062f\u06cc\u062f \u06cc\u0627\u062f\u06af\u06cc\u0631\u0646\u062f\u0647 \u067e\u0646\u0647\u0627\u0646 \u0634\u062f\u0647 \u0627\u0633\u062a", "Explore Programs": "\u06a9\u0627\u0648\u0634 \u0628\u0631\u0646\u0627\u0645\u0647\u200c\u0647\u0627", - "Explore courses": "\u06a9\u0627\u0648\u0634 \u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc \u0622\u0645\u0648\u0632\u0634\u06cc", "Explore new programs": "\u0628\u0631\u0646\u0627\u0645\u0647 \u0647\u0627\u06cc \u062c\u062f\u06cc\u062f \u0631\u0627 \u06a9\u0627\u0648\u0634 \u06a9\u0646\u06cc\u062f", "Explore subscription options": "\u06af\u0632\u06cc\u0646\u0647 \u0647\u0627\u06cc \u0627\u0634\u062a\u0631\u0627\u06a9 \u0631\u0627 \u06a9\u0627\u0648\u0634 \u06a9\u0646\u06cc\u062f", "Explore your course!": "\u062f\u0648\u0631\u0647\u200c\u062a\u0627\u0646 \u0631\u0627 \u0628\u06cc\u0627\u0628\u06cc\u062f!", @@ -1559,7 +1555,6 @@ "Reason for change:": "\u0639\u0644\u062a \u062a\u063a\u06cc\u06cc\u0631: ", "Receive updates": "\u062f\u0631\u06cc\u0627\u0641\u062a \u0646\u0633\u062e\u0647\u200c\u0647\u0627\u06cc \u0631\u0648\u0632\u0622\u0645\u062f", "Recent Activity": "\u0641\u0639\u0627\u0644\u06cc\u062a \u0627\u062e\u06cc\u0631", - "Recommendations for you": "\u062a\u0648\u0635\u06cc\u0647\u200c\u0647\u0627\u06cc\u06cc \u0628\u0631\u0627\u06cc \u0634\u0645\u0627", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": " \u067e\u06cc\u0634\u0646\u0647\u0627\u062f \u0645\u0627 \u0628\u0631\u0627\u06cc \u0648\u0636\u0648\u062d \u062a\u0635\u0648\u06cc\u0631 {imageResolution} \u0627\u0633\u062a\u060c \u0628\u06cc\u0634\u06cc\u0646\u06c0 \u0627\u0646\u062f\u0627\u0632\u06c0\u0647 \u067e\u0631\u0648\u0646\u062f\u06c0 \u062a\u0635\u0648\u06cc\u0631 \u0628\u0627\u06cc\u062f {maxFileSize} \u0648 \u0642\u0627\u0644\u0628 \u0628\u0627\u06cc\u062f \u06cc\u06a9\u06cc \u0627\u0632 {supportedImageFormats} \u0628\u0627\u0634\u062f.", "Recover my password": "\u06af\u0630\u0631\u0648\u0627\u0698\u06c0 \u0645\u0631\u0627 \u0628\u0627\u0632\u06cc\u0627\u0628\u06cc \u06a9\u0646", "Recovery Email Address": "\u0628\u0627\u0632\u06cc\u0627\u0628\u06cc \u0646\u0634\u0627\u0646\u06cc \u0631\u0627\u06cc\u0627\u0646\u0627\u0645\u0647", @@ -1932,7 +1927,6 @@ "The following errors were generated:": "\u0627\u06cc\u0646 \u062e\u0637\u0627\u0647\u0627 \u0631\u062e \u062f\u0627\u062f\u0647\u200c\u0627\u0646\u062f:", "The following file types are not allowed: ": "\u0627\u06cc\u0646 \u0646\u0648\u0639 \u067e\u0631\u0648\u0646\u062f\u0647 \u0645\u062c\u0627\u0632 \u0646\u06cc\u0633\u062a.", "The following files already exist in this course but don't match the version used by the component you pasted:": "\u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0632\u06cc\u0631 \u0627\u0632 \u0642\u0628\u0644 \u062f\u0631 \u0627\u06cc\u0646 \u062f\u0648\u0631\u0647 \u0648\u062c\u0648\u062f \u062f\u0627\u0631\u0646\u062f\u060c \u0627\u0645\u0627 \u0628\u0627 \u0646\u0633\u062e\u0647 \u0627\u0633\u062a\u0641\u0627\u062f\u0647 \u0634\u062f\u0647 \u062a\u0648\u0633\u0637 \u0645\u0624\u0644\u0641\u0647\u200c\u0627\u06cc \u06a9\u0647 \u0686\u0633\u0628\u0627\u0646\u062f\u0647\u200c\u0627\u06cc\u062f \u0645\u0637\u0627\u0628\u0642\u062a \u0646\u062f\u0627\u0631\u0646\u062f:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "\u0627\u0637\u0644\u0627\u0639\u0627\u062a \u0632\u06cc\u0631 \u0642\u0628\u0644\u0627\u064b \u0628\u062e\u0634\u06cc \u0627\u0632 \u067e\u0631\u0648\u0646\u062f\u06c0 \u06a9\u0627\u0631\u0628\u0631\u06cc {platform} \u0634\u0645\u0627\u0633\u062a. \u0622\u0646 \u0631\u0627 \u062f\u0631 \u0627\u06cc\u0646\u062c\u0627 \u0628\u0631\u0627\u06cc \u0628\u0631\u0646\u0627\u0645\u0647 \u0634\u0645\u0627 \u0642\u0631\u0627\u0631 \u062f\u0627\u062f\u0647\u200c\u0627\u06cc\u0645.", "The following message will be displayed at the bottom of the courseware pages within your course:": "\u067e\u06cc\u0627\u0645 \u0632\u06cc\u0631 \u062f\u0631 \u067e\u0627\u06cc\u06cc\u0646 \u0635\u0641\u062d\u06c0 \u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc \u0622\u0645\u0648\u0632\u0634\u06cc \u0645\u0631\u0628\u0648\u0637 \u0628\u0647 \u062f\u0648\u0631\u06c0 \u0634\u0645\u0627 \u0646\u0645\u0627\u06cc\u0634 \u062f\u0627\u062f\u0647 \u0645\u06cc\u200c\u0634\u0648\u062f:", "The following options are available for the {license_name} license.": "\u06af\u0632\u06cc\u0646\u0647\u200c\u0647\u0627\u06cc \u0632\u06cc\u0631 \u0628\u0631\u0627\u06cc {license_name} \u0641\u0631\u0627\u0647\u0645 \u0647\u0633\u062a\u0646\u062f.", "The following required files could not be added to the course:": "\u0641\u0627\u06cc\u0644 \u0647\u0627\u06cc \u0645\u0648\u0631\u062f \u0646\u06cc\u0627\u0632 \u0632\u06cc\u0631 \u0631\u0627 \u0646\u0645\u06cc \u062a\u0648\u0627\u0646 \u0628\u0647 \u062f\u0648\u0631\u0647 \u0627\u0636\u0627\u0641\u0647 \u06a9\u0631\u062f:", @@ -2582,7 +2576,6 @@ "internally reviewed": "\u0628\u0647\u200c\u0635\u0648\u0631\u062a \u062f\u0627\u062e\u0644\u06cc \u0628\u0631\u0631\u0633\u06cc \u0634\u062f", "last activity": "\u0622\u062e\u0631\u06cc\u0646 \u0641\u0639\u0627\u0644\u06cc\u062a", "less than a minute": "\u06a9\u0645\u062a\u0631 \u0627\u0632 \u06cc\u06a9 \u062f\u0642\u06cc\u0642\u0647", - "loading": "\u062f\u0631 \u062d\u0627\u0644 \u0628\u0627\u0631\u06af\u06cc\u0631\u06cc", "marked as answer %(time_ago)s": "\u0646\u0634\u0627\u0646\u062f\u0627\u0631 \u0628\u0647\u200c\u0639\u0646\u0648\u0627\u0646 \u067e\u0627\u0633\u062e%(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "\u0646\u0634\u0627\u0646\u062f\u0627\u0631 \u0628\u0647 \u0639\u0646\u0648\u0627\u0646 \u067e\u0627\u0633\u062e %(time_ago)s \u062a\u0648\u0633\u0637 %(user)s", "minute": "\u062f\u0642\u06cc\u0642\u0647", diff --git a/cms/static/js/i18n/fa/djangojs.js b/cms/static/js/i18n/fa/djangojs.js index 6714c1afaec4..f688b97f0e87 100644 --- a/cms/static/js/i18n/fa/djangojs.js +++ b/cms/static/js/i18n/fa/djangojs.js @@ -242,7 +242,6 @@ "API Secret": "API Secret", "Abbreviation": "\u0633\u0631\u0646\u0627\u0645", "About Me": "\u062f\u0631\u0628\u0627\u0631\u0647 \u0645\u0646", - "About You": "\u062f\u0631\u0628\u0627\u0631\u06c0 \u062a\u0648", "About me": "\u062f\u0631\u0628\u0627\u0631\u06c0 \u0645\u0646", "Access to some content in this unit is restricted to specific groups of learners": "\u062f\u0633\u062a\u0631\u0633\u06cc \u0628\u0647 \u0627\u06cc\u0646 \u0648\u0627\u062d\u062f \u0645\u062d\u062f\u0648\u062f \u0628\u0647 \u06af\u0631\u0648\u0647\u200c\u0647\u0627\u06cc \u0648\u06cc\u0698\u0647\u200c\u0627\u06cc \u0627\u0632 \u06cc\u0627\u062f\u06af\u06cc\u0631\u0646\u062f\u06af\u0627\u0646 \u0627\u0633\u062a", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u062f\u0633\u062a\u0631\u0633\u06cc \u0628\u0647 \u0627\u06cc\u0646 {blockType} \u0645\u062d\u062f\u0648\u062f \u0628\u0647 \u06af\u0631\u0648\u0647 \u0648\u06cc\u0698\u0647\u200c\u0627\u06cc \u0627\u0632 \u06cc\u0627\u062f\u06af\u06cc\u0631\u0646\u062f\u06af\u0627\u0646 \u0627\u0633\u062a .", @@ -422,7 +421,6 @@ "Average": "\u0645\u06cc\u0627\u0646\u06af\u06cc\u0646", "Back to Full List": "\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u0641\u0647\u0631\u0633\u062a \u06a9\u0627\u0645\u0644", "Back to sign in": "\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u0642\u0633\u0645\u062a \u0648\u0631\u0648\u062f \u0628\u0647 \u0633\u0627\u0645\u0627\u0646\u0647", - "Back to {platform} FAQs": "\u0628\u0631\u06af\u0631\u062f \u0628\u0647 \u0633\u0624\u0627\u0644\u0627\u062a \u067e\u0631\u062a\u06a9\u0631\u0627\u0631 {platform}", "Background color": "\u0631\u0646\u06af \u067e\u0633\u200c\u0632\u0645\u06cc\u0646\u0647", "Basic": "\u0627\u0628\u062a\u062f\u0627\u06cc\u06cc", "Basic Account Information": "\u0627\u0637\u0644\u0627\u0639\u0627\u062a \u06a9\u0627\u0631\u0628\u0631\u06cc \u0627\u0648\u0644\u06cc\u0647", @@ -447,7 +445,6 @@ "Border color": "\u0631\u0646\u06af \u062d\u0627\u0634\u06cc\u0647", "Bottom": "\u067e\u0627\u06cc\u06cc\u0646", "Browse": "\u062a\u0648\u0631\u0642", - "Browse recently launched courses and see what's new in your favorite subjects.": "\u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc\u06cc \u0631\u0627 \u06a9\u0647 \u0627\u062e\u06cc\u0631\u0627\u064b \u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc \u0634\u062f\u0647\u200c\u0627\u0646\u062f \u0645\u0631\u0648\u0631 \u06a9\u0646\u06cc\u062f \u0648 \u0628\u0628\u06cc\u0646\u06cc\u062f \u0686\u0647 \u0645\u0648\u0627\u0631\u062f \u062c\u062f\u06cc\u062f\u06cc \u062f\u0631 \u0645\u0648\u0636\u0648\u0639\u0627\u062a \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u06c0 \u0634\u0645\u0627 \u0648\u062c\u0648\u062f \u062f\u0627\u0631\u062f.", "Browse recently launched courses and see what\\'s new in your favorite subjects": "\u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc\u06cc \u06a9\u0647 \u0627\u062e\u06cc\u0631\u0627\u064b \u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc\u200c\u0634\u062f\u0647\u200c \u0631\u0627 \u0645\u0631\u0648\u0631 \u06a9\u0631\u062f\u0647 \u0648 \u0645\u0648\u0636\u0648\u0639\u0627\u062a \u062c\u062f\u06cc\u062f \u0645\u0648\u0631\u062f \u0639\u0644\u0627\u0642\u0647 \u062e\u0648\u062f \u0631\u0627 \u0628\u0628\u06cc\u0646\u06cc\u062f", "Browsing": "\u062a\u0648\u0631\u0642", "Bulk Exceptions": "\u0627\u0633\u062a\u062b\u0646\u0627\u0626\u0627\u062a \u0627\u0646\u0628\u0648\u0647", @@ -909,7 +906,6 @@ "Explanation": "\u062a\u0648\u0636\u06cc\u062d", "Explicitly Hiding from Students": "\u0635\u0631\u06cc\u062d\u0627 \u0627\u0632 \u062f\u06cc\u062f \u06cc\u0627\u062f\u06af\u06cc\u0631\u0646\u062f\u0647 \u067e\u0646\u0647\u0627\u0646 \u0634\u062f\u0647 \u0627\u0633\u062a", "Explore Programs": "\u06a9\u0627\u0648\u0634 \u0628\u0631\u0646\u0627\u0645\u0647\u200c\u0647\u0627", - "Explore courses": "\u06a9\u0627\u0648\u0634 \u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc \u0622\u0645\u0648\u0632\u0634\u06cc", "Explore new programs": "\u0628\u0631\u0646\u0627\u0645\u0647 \u0647\u0627\u06cc \u062c\u062f\u06cc\u062f \u0631\u0627 \u06a9\u0627\u0648\u0634 \u06a9\u0646\u06cc\u062f", "Explore subscription options": "\u06af\u0632\u06cc\u0646\u0647 \u0647\u0627\u06cc \u0627\u0634\u062a\u0631\u0627\u06a9 \u0631\u0627 \u06a9\u0627\u0648\u0634 \u06a9\u0646\u06cc\u062f", "Explore your course!": "\u062f\u0648\u0631\u0647\u200c\u062a\u0627\u0646 \u0631\u0627 \u0628\u06cc\u0627\u0628\u06cc\u062f!", @@ -1559,7 +1555,6 @@ "Reason for change:": "\u0639\u0644\u062a \u062a\u063a\u06cc\u06cc\u0631: ", "Receive updates": "\u062f\u0631\u06cc\u0627\u0641\u062a \u0646\u0633\u062e\u0647\u200c\u0647\u0627\u06cc \u0631\u0648\u0632\u0622\u0645\u062f", "Recent Activity": "\u0641\u0639\u0627\u0644\u06cc\u062a \u0627\u062e\u06cc\u0631", - "Recommendations for you": "\u062a\u0648\u0635\u06cc\u0647\u200c\u0647\u0627\u06cc\u06cc \u0628\u0631\u0627\u06cc \u0634\u0645\u0627", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": " \u067e\u06cc\u0634\u0646\u0647\u0627\u062f \u0645\u0627 \u0628\u0631\u0627\u06cc \u0648\u0636\u0648\u062d \u062a\u0635\u0648\u06cc\u0631 {imageResolution} \u0627\u0633\u062a\u060c \u0628\u06cc\u0634\u06cc\u0646\u06c0 \u0627\u0646\u062f\u0627\u0632\u06c0\u0647 \u067e\u0631\u0648\u0646\u062f\u06c0 \u062a\u0635\u0648\u06cc\u0631 \u0628\u0627\u06cc\u062f {maxFileSize} \u0648 \u0642\u0627\u0644\u0628 \u0628\u0627\u06cc\u062f \u06cc\u06a9\u06cc \u0627\u0632 {supportedImageFormats} \u0628\u0627\u0634\u062f.", "Recover my password": "\u06af\u0630\u0631\u0648\u0627\u0698\u06c0 \u0645\u0631\u0627 \u0628\u0627\u0632\u06cc\u0627\u0628\u06cc \u06a9\u0646", "Recovery Email Address": "\u0628\u0627\u0632\u06cc\u0627\u0628\u06cc \u0646\u0634\u0627\u0646\u06cc \u0631\u0627\u06cc\u0627\u0646\u0627\u0645\u0647", @@ -1932,7 +1927,6 @@ "The following errors were generated:": "\u0627\u06cc\u0646 \u062e\u0637\u0627\u0647\u0627 \u0631\u062e \u062f\u0627\u062f\u0647\u200c\u0627\u0646\u062f:", "The following file types are not allowed: ": "\u0627\u06cc\u0646 \u0646\u0648\u0639 \u067e\u0631\u0648\u0646\u062f\u0647 \u0645\u062c\u0627\u0632 \u0646\u06cc\u0633\u062a.", "The following files already exist in this course but don't match the version used by the component you pasted:": "\u0641\u0627\u06cc\u0644\u200c\u0647\u0627\u06cc \u0632\u06cc\u0631 \u0627\u0632 \u0642\u0628\u0644 \u062f\u0631 \u0627\u06cc\u0646 \u062f\u0648\u0631\u0647 \u0648\u062c\u0648\u062f \u062f\u0627\u0631\u0646\u062f\u060c \u0627\u0645\u0627 \u0628\u0627 \u0646\u0633\u062e\u0647 \u0627\u0633\u062a\u0641\u0627\u062f\u0647 \u0634\u062f\u0647 \u062a\u0648\u0633\u0637 \u0645\u0624\u0644\u0641\u0647\u200c\u0627\u06cc \u06a9\u0647 \u0686\u0633\u0628\u0627\u0646\u062f\u0647\u200c\u0627\u06cc\u062f \u0645\u0637\u0627\u0628\u0642\u062a \u0646\u062f\u0627\u0631\u0646\u062f:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "\u0627\u0637\u0644\u0627\u0639\u0627\u062a \u0632\u06cc\u0631 \u0642\u0628\u0644\u0627\u064b \u0628\u062e\u0634\u06cc \u0627\u0632 \u067e\u0631\u0648\u0646\u062f\u06c0 \u06a9\u0627\u0631\u0628\u0631\u06cc {platform} \u0634\u0645\u0627\u0633\u062a. \u0622\u0646 \u0631\u0627 \u062f\u0631 \u0627\u06cc\u0646\u062c\u0627 \u0628\u0631\u0627\u06cc \u0628\u0631\u0646\u0627\u0645\u0647 \u0634\u0645\u0627 \u0642\u0631\u0627\u0631 \u062f\u0627\u062f\u0647\u200c\u0627\u06cc\u0645.", "The following message will be displayed at the bottom of the courseware pages within your course:": "\u067e\u06cc\u0627\u0645 \u0632\u06cc\u0631 \u062f\u0631 \u067e\u0627\u06cc\u06cc\u0646 \u0635\u0641\u062d\u06c0 \u062f\u0648\u0631\u0647\u200c\u0647\u0627\u06cc \u0622\u0645\u0648\u0632\u0634\u06cc \u0645\u0631\u0628\u0648\u0637 \u0628\u0647 \u062f\u0648\u0631\u06c0 \u0634\u0645\u0627 \u0646\u0645\u0627\u06cc\u0634 \u062f\u0627\u062f\u0647 \u0645\u06cc\u200c\u0634\u0648\u062f:", "The following options are available for the {license_name} license.": "\u06af\u0632\u06cc\u0646\u0647\u200c\u0647\u0627\u06cc \u0632\u06cc\u0631 \u0628\u0631\u0627\u06cc {license_name} \u0641\u0631\u0627\u0647\u0645 \u0647\u0633\u062a\u0646\u062f.", "The following required files could not be added to the course:": "\u0641\u0627\u06cc\u0644 \u0647\u0627\u06cc \u0645\u0648\u0631\u062f \u0646\u06cc\u0627\u0632 \u0632\u06cc\u0631 \u0631\u0627 \u0646\u0645\u06cc \u062a\u0648\u0627\u0646 \u0628\u0647 \u062f\u0648\u0631\u0647 \u0627\u0636\u0627\u0641\u0647 \u06a9\u0631\u062f:", @@ -2582,7 +2576,6 @@ "internally reviewed": "\u0628\u0647\u200c\u0635\u0648\u0631\u062a \u062f\u0627\u062e\u0644\u06cc \u0628\u0631\u0631\u0633\u06cc \u0634\u062f", "last activity": "\u0622\u062e\u0631\u06cc\u0646 \u0641\u0639\u0627\u0644\u06cc\u062a", "less than a minute": "\u06a9\u0645\u062a\u0631 \u0627\u0632 \u06cc\u06a9 \u062f\u0642\u06cc\u0642\u0647", - "loading": "\u062f\u0631 \u062d\u0627\u0644 \u0628\u0627\u0631\u06af\u06cc\u0631\u06cc", "marked as answer %(time_ago)s": "\u0646\u0634\u0627\u0646\u062f\u0627\u0631 \u0628\u0647\u200c\u0639\u0646\u0648\u0627\u0646 \u067e\u0627\u0633\u062e%(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "\u0646\u0634\u0627\u0646\u062f\u0627\u0631 \u0628\u0647 \u0639\u0646\u0648\u0627\u0646 \u067e\u0627\u0633\u062e %(time_ago)s \u062a\u0648\u0633\u0637 %(user)s", "minute": "\u062f\u0642\u06cc\u0642\u0647", diff --git a/cms/static/js/i18n/fr/djangojs.js b/cms/static/js/i18n/fr/djangojs.js index a2986452be21..eac21e828639 100644 --- a/cms/static/js/i18n/fr/djangojs.js +++ b/cms/static/js/i18n/fr/djangojs.js @@ -239,7 +239,6 @@ "API Secret": "Secret de l'API", "Abbreviation": "Abr\u00e9viation", "About Me": "\u00c0 propos de moi", - "About You": "A propos de vous", "About me": "A propos de moi", "Access to some content in this unit is restricted to specific groups of learners": "L'acc\u00e8s \u00e0 une partie du contenu de cette unit\u00e9 est restreint \u00e0 un groupe d'\u00e9tudiants sp\u00e9cifique.", "Access to some content in this {blockType} is restricted to specific groups of learners.": "L'acc\u00e8s \u00e0 une partie du contenu de ce {blockType} est restreint \u00e0 un groupe d'\u00e9tudiants sp\u00e9cifique.", @@ -416,7 +415,6 @@ "Average": "Moyen", "Back to Full List": "Retour \u00e0 la liste compl\u00e8te", "Back to sign in": "Retour \u00e0 la connexion", - "Back to {platform} FAQs": "Retour vers la page FAQ de {platform}", "Background color": "Couleur du fond", "Basic": "Basique", "Basic Account Information": "Informations g\u00e9n\u00e9rales du compte", @@ -1858,7 +1856,6 @@ "The following email addresses and/or usernames are invalid:": "Les adresses email et/ou noms d'utilisateurs suivants sont invalides :", "The following errors were generated:": "Les erreurs suivantes ont \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9es:", "The following file types are not allowed: ": "Les types de fichiers suivants ne sont pas support\u00e9s :", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "Les informations suivantes sont d\u00e9j\u00e0 int\u00e9gr\u00e9es dans votre profil de {platform}. Nous les avons inclus ici pour votre demande.", "The following message will be displayed at the bottom of the courseware pages within your course:": "Le message suivant sera affich\u00e9 au bas des pages du cours :", "The following options are available for the {license_name} license.": "Les options suivantes sont disponibles pour la licence {license_name}", "The following users are no longer enrolled in the course:": "Les utilisateurs suivants ont \u00e9t\u00e9 d\u00e9sinscrits du cours\u00a0:", diff --git a/cms/static/js/i18n/id/djangojs.js b/cms/static/js/i18n/id/djangojs.js index fa26e7240789..101066c6ad08 100644 --- a/cms/static/js/i18n/id/djangojs.js +++ b/cms/static/js/i18n/id/djangojs.js @@ -86,7 +86,6 @@ "API Secret": "Rahasia API", "Abbreviation": "Singkatan", "About Me": "Tentang Saya", - "About You": "Tentang Anda", "About me": "Tentang saya", "Account": "Akun", "Account Information": "Informasi Akun", @@ -207,7 +206,6 @@ "Average": "Sedang", "Back to Full List": "Kembali ke Daftar Penuh", "Back to sign in": "Kembali masuk", - "Back to {platform} FAQs": "Kembali ke FAQ {platform}", "Background color": "Warna latar belakang", "Basic": "Dasar", "Basic Account Information": "Informasi Akun Dasar", diff --git a/cms/static/js/i18n/it-it/djangojs.js b/cms/static/js/i18n/it-it/djangojs.js index 771c73e6c2eb..dc63c6ad785a 100644 --- a/cms/static/js/i18n/it-it/djangojs.js +++ b/cms/static/js/i18n/it-it/djangojs.js @@ -240,7 +240,6 @@ "API Secret": "API Secret", "Abbreviation": "Abbreviazione", "About Me": "Su di me", - "About You": "Informazioni personali ", "About me": "Su di me", "Access to some content in this unit is restricted to specific groups of learners": "L'accesso ad alcuni contenuti in questa unit\u00e0 \u00e8 limitato a gruppi di studenti specifici.", "Access to some content in this {blockType} is restricted to specific groups of learners.": "L'accesso ad alcuni contenuti in questo {blockType} \u00e8 limitato a gruppi di studenti specifici.", @@ -418,7 +417,6 @@ "Average": "Normale", "Back to Full List": "Torna all'elenco completo ", "Back to sign in": "Torna all'accesso ", - "Back to {platform} FAQs": "Torna alle FAQ {platform} ", "Background color": "Colore di sfondo", "Basic": "Di base", "Basic Account Information": "Informazioni di base", @@ -894,7 +892,6 @@ "Explanation": "Spiegazione", "Explicitly Hiding from Students": "Dati nascosti esplicitamente agli studenti", "Explore Programs": "Esplora programmi ", - "Explore courses": "Esplora corsi", "Explore your course!": "Esplora il tuo corso! ", "Failed Proctoring": "Supervisione non superata", "Failed to clone rubric": "Impossibile clonare la rubrica", @@ -1521,7 +1518,6 @@ "Reason for change:": "Motivo della modifica: ", "Receive updates": "Ricevi aggiornamenti ", "Recent Activity": "Attivit\u00e0 recente", - "Recommendations for you": "Consigli per te", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "La risoluzione consigliata per l'immagine \u00e8 {imageResolution}, la dimensione massima del file dell'immagine deve essere {maxFileSize} e il formato deve essere uno tra {supportedImageFormats}. ", "Recover my password": "Recupera password", "Recovery Email Address": "Indirizzo email di recupero", @@ -1874,7 +1870,6 @@ "The following email addresses and/or usernames are invalid:": "I seguenti indirizzi email e/o nomi utente non sono validi:", "The following errors were generated:": "Sono stati generati i seguenti errori:", "The following file types are not allowed: ": "I seguenti tipi di file non sono ammessi:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "Le seguenti informazioni fanno gi\u00e0 parte del tuo profilo di {platform}. Sono state incluse qui per la tua applicazione.", "The following message will be displayed at the bottom of the courseware pages within your course:": "Il seguente messaggio verr\u00e0 visualizzato nella parte inferiore delle pagine all'interno del corso: ", "The following options are available for the {license_name} license.": "Per la licenza {license_name} sono disponibili le seguenti opzioni.", "The following users are no longer enrolled in the course:": "Gli utenti seguenti non sono pi\u00f9 iscritti al corso:", @@ -2492,7 +2487,6 @@ "internally reviewed": "verificato internamente", "last activity": "ultima attivit\u00e0", "less than a minute": "meno di un minuto", - "loading": "Caricamento in corso", "marked as answer %(time_ago)s": "contrassegnato come risposta %(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "contrassegnato come risposta %(time_ago)s da %(user)s", "minute": "minuto", diff --git a/cms/static/js/i18n/ja-jp/djangojs.js b/cms/static/js/i18n/ja-jp/djangojs.js index ffc163bc6d1c..6f4820383e2f 100644 --- a/cms/static/js/i18n/ja-jp/djangojs.js +++ b/cms/static/js/i18n/ja-jp/djangojs.js @@ -58,7 +58,6 @@ "ABCDEFGHIJKLMNOPQRSTUVWXYZ": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "Abbreviation": "\u7565\u79f0", "About Me": "\u81ea\u5df1\u7d39\u4ecb", - "About You": "\u3042\u306a\u305f\u306b\u3064\u3044\u3066", "About me": "\u81ea\u5df1\u7d39\u4ecb", "Access to some content in this unit is restricted to specific groups of learners": "\u3053\u306e\u30e6\u30cb\u30c3\u30c8\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306f\u7279\u5b9a\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u53d7\u8b1b\u751f\u306b\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u3053\u306e {blockType} \u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306f\u7279\u5b9a\u306e\u30b0\u30eb\u30fc\u30d7\u306e\u53d7\u8b1b\u751f\u306b\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u3002", @@ -174,7 +173,6 @@ "Available %s": "\u5229\u7528\u53ef\u80fd %s", "Back to Full List": "\u5168\u30ea\u30b9\u30c8\u3078\u623b\u308b", "Back to sign in": "\u30b5\u30a4\u30f3\u30a4\u30f3\u3078\u623b\u308b", - "Back to {platform} FAQs": "{platform} FAQ\u3078\u623b\u308b", "Basic": "\u57fa\u672c", "Basic Account Information": "\u30a2\u30ab\u30a6\u30f3\u30c8\u57fa\u672c\u60c5\u5831", "Be sure your entire face is inside the frame": "\u9854\u5168\u4f53\u304c\u30d5\u30ec\u30fc\u30e0\u5185\u306b\u5165\u3063\u3066\u3044\u308b\u304b\u78ba\u8a8d", diff --git a/cms/static/js/i18n/pl/djangojs.js b/cms/static/js/i18n/pl/djangojs.js index b8a76c3b2d42..d9d5e4e8c5cb 100644 --- a/cms/static/js/i18n/pl/djangojs.js +++ b/cms/static/js/i18n/pl/djangojs.js @@ -81,7 +81,6 @@ "API Secret": "Has\u0142o API", "Abbreviation": "Skr\u00f3t", "About Me": "O mnie", - "About You": "O tobie", "About me": "O mnie", "Access to some content in this unit is restricted to specific groups of learners": "Dost\u0119p do niekt\u00f3rych tre\u015bci tej lekcji jest mo\u017cliwy wy\u0142\u0105cznie dla okre\u015blonych grup student\u00f3w.", "Access to some content in this {blockType} is restricted to specific groups of learners.": "Dost\u0119p do niekt\u00f3rych tre\u015bci w tym {blockType} jest mo\u017cliwy wy\u0142\u0105cznie dla wybranych grup student\u00f3w.", @@ -211,7 +210,6 @@ "Available %s": "Dost\u0119pne %s", "Back to Full List": "Powr\u00f3t do pe\u0142nej listy", "Back to sign in": "Powr\u00f3t do strony logowania", - "Back to {platform} FAQs": "Powr\u00f3t do PiO {platform}", "Basic": "Podstawowy", "Basic Account Information": "Podstawowe dane konta", "Be sure your entire face is inside the frame": "Upewnij si\u0119, \u017ce ca\u0142a twarz znajduje si\u0119 w ramce.", @@ -1630,7 +1628,6 @@ "group configuration": "konfiguracja grupy", "image omitted": "pomini\u0119ty obrazek", "last activity": "ostatnia aktywno\u015b\u0107", - "loading": "\u0142adowanie", "minute": "minuta", "minutes": "minut/y", "name": "nazwa", diff --git a/cms/static/js/i18n/pt-br/djangojs.js b/cms/static/js/i18n/pt-br/djangojs.js index 821d36a522cc..51a758f0bb0e 100644 --- a/cms/static/js/i18n/pt-br/djangojs.js +++ b/cms/static/js/i18n/pt-br/djangojs.js @@ -46,7 +46,6 @@ "A valid email address is required": "\u00c9 preciso um endere\u00e7o de e-mail v\u00e1lido", "ABCDEFGHIJKLMNOPQRSTUVWXYZ": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "Abbreviation": "Abrevia\u00e7\u00e3o", - "About You": "Sobre Voc\u00ea", "Account Information": "Informa\u00e7\u00f5es da Conta", "Account Not Activated": "Conta n\u00e3o ativada", "Account Settings": "Configura\u00e7\u00f5es da Conta", @@ -120,7 +119,6 @@ "Automatic": "Autom\u00e1tico", "Available %s": "%s dispon\u00edveis", "Back to sign in": "Voltar para entrar", - "Back to {platform} FAQs": "Voltar para {platform} FAQs", "Be sure your entire face is inside the frame": "Certifique-se que o seu rosto todo esteja dentro dos limites da borda", "Before proceeding, please confirm that your details match": "Antes de prosseguir, confirme os seus dados", "Before you upgrade to a certificate track, you must activate your account.": "Antes de atualizar seu certificado seguinte, voc\u00ea deve ativar sua conta.", diff --git a/cms/static/js/i18n/pt-pt/djangojs.js b/cms/static/js/i18n/pt-pt/djangojs.js index c4f5af47fc8f..2b57f29feb2a 100644 --- a/cms/static/js/i18n/pt-pt/djangojs.js +++ b/cms/static/js/i18n/pt-pt/djangojs.js @@ -241,7 +241,6 @@ "API Secret": " API Secreto", "Abbreviation": "Abrevia\u00e7\u00e3o", "About Me": "Sobre Mim", - "About You": "Sobre Si", "About me": "Sobre mim", "Access to some content in this unit is restricted to specific groups of learners": "O acesso a algum conte\u00fado nesta unidade \u00e9 restrito a grupos espec\u00edficos de estudantes", "Access to some content in this {blockType} is restricted to specific groups of learners.": "O acesso a algum conte\u00fado neste {blockType} \u00e9 restrito a grupos espec\u00edficos de estudantes.", @@ -419,7 +418,6 @@ "Average": "M\u00e9dia", "Back to Full List": "Voltar \u00e0 lista completa", "Back to sign in": "Voltar para iniciar sess\u00e3o", - "Back to {platform} FAQs": "Voltar para FAQs {platform}", "Background color": "Cor de fundo", "Basic": "B\u00e1sico", "Basic Account Information": "Informa\u00e7\u00e3o b\u00e1sica da conta", @@ -444,7 +442,6 @@ "Border color": "Cor da borda", "Bottom": "Fundo", "Browse": "Procurar", - "Browse recently launched courses and see what's new in your favorite subjects.": "Explore os cursos recentemente lan\u00e7ados e veja o que h\u00e1 de novo sobre os seus temas favoritos", "Browse recently launched courses and see what\\'s new in your favorite subjects": "Explore os cursos recentemente lan\u00e7ados e veja o que h\u00e1 de novo sobre os seus temas favoritos", "Browsing": "Navegar", "Bulk Exceptions": "Exce\u00e7\u00f5es em massa", @@ -897,7 +894,6 @@ "Explanation": "Explica\u00e7\u00e3o", "Explicitly Hiding from Students": "Ocultar Explicitamente dos Estudantes", "Explore Programs": "Explorar Programas", - "Explore courses": "Explorar cursos", "Explore your course!": "Explore o seu curso!", "Failed Proctoring": "Supervis\u00e3o Falhou", "Failed to clone rubric": "Falha na clonagem da rubrica", @@ -1526,7 +1522,6 @@ "Reason for change:": "Motivo da altera\u00e7\u00e3o:", "Receive updates": "Receba atualiza\u00e7\u00f5es", "Recent Activity": "Atividade recente", - "Recommendations for you": "Recomenda\u00e7\u00f5es para ti", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "A resolu\u00e7\u00e3o de imagem recomendada \u00e9 {imageResolution}, o tamanho m\u00e1ximo do ficheiro de imagem deve ser {maxFileSize} e o formato deve ser um dos {supportedImageFormats}.", "Recover my password": "Recuperar a minha palavra-passe", "Recovery Email Address": "Endere\u00e7o de E-mail de Recupera\u00e7\u00e3o", @@ -1879,7 +1874,6 @@ "The following email addresses and/or usernames are invalid:": "Os seguintes endere\u00e7os de email e/ou nomes de utilizador s\u00e3o inv\u00e1lidos:", "The following errors were generated:": "Os seguintes erros foram gerados:", "The following file types are not allowed: ": "Os seguintes tipos de ficheiro n\u00e3o s\u00e3o permitidos: ", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "As seguintes informa\u00e7\u00f5es j\u00e1 fazem parte do seu perfil {platform}. Inclu\u00edmo-la aqui para a sua inscri\u00e7\u00e3o.", "The following message will be displayed at the bottom of the courseware pages within your course:": "A seguinte mensagem ser\u00e1 apresentada na parte inferior das p\u00e1ginas do material did\u00e1tico dentro do seu curso:", "The following options are available for the {license_name} license.": "As seguintes op\u00e7\u00f5es est\u00e3o dispon\u00edveis para a licen\u00e7a {license_name}.", "The following users are no longer enrolled in the course:": "Os seguintes utilizadores j\u00e1 n\u00e3o est\u00e3o inscritos no curso:", @@ -2487,7 +2481,6 @@ "internally reviewed": "revisto internamente", "last activity": "\u00faltima atividade", "less than a minute": "menos de um minuto", - "loading": "a carregar", "marked as answer %(time_ago)s": "marcado como resposta %(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "marcado como resposta %(time_ago)s por %(user)s", "minute": "minuto", diff --git a/cms/static/js/i18n/rtl/djangojs.js b/cms/static/js/i18n/rtl/djangojs.js index 2cd28a576a4a..de6c6da8ec5e 100644 --- a/cms/static/js/i18n/rtl/djangojs.js +++ b/cms/static/js/i18n/rtl/djangojs.js @@ -119,7 +119,6 @@ "API Secret": "\u023a\u2c63\u0197 S\u01dd\u0254\u0279\u01dd\u0287", "Abbreviation": "\u023abb\u0279\u01dd\u028c\u1d09\u0250\u0287\u1d09\u00f8n", "About Me": "\u023ab\u00f8n\u0287 M\u01dd", - "About You": "\u023ab\u00f8n\u0287 \u024e\u00f8n", "About me": "\u023ab\u00f8n\u0287 \u026f\u01dd", "Access to some content in this unit is restricted to specific groups of learners": "\u023a\u0254\u0254\u01ddss \u0287\u00f8 s\u00f8\u026f\u01dd \u0254\u00f8n\u0287\u01ddn\u0287 \u1d09n \u0287\u0265\u1d09s nn\u1d09\u0287 \u1d09s \u0279\u01dds\u0287\u0279\u1d09\u0254\u0287\u01ddd \u0287\u00f8 sd\u01dd\u0254\u1d09\u025f\u1d09\u0254 \u0183\u0279\u00f8nds \u00f8\u025f l\u01dd\u0250\u0279n\u01dd\u0279s", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u023a\u0254\u0254\u01ddss \u0287\u00f8 s\u00f8\u026f\u01dd \u0254\u00f8n\u0287\u01ddn\u0287 \u1d09n \u0287\u0265\u1d09s {blockType} \u1d09s \u0279\u01dds\u0287\u0279\u1d09\u0254\u0287\u01ddd \u0287\u00f8 sd\u01dd\u0254\u1d09\u025f\u1d09\u0254 \u0183\u0279\u00f8nds \u00f8\u025f l\u01dd\u0250\u0279n\u01dd\u0279s.", @@ -249,6 +248,7 @@ "Any course progress or grades from your current session will be lost.": "\u023an\u028e \u0254\u00f8n\u0279s\u01dd d\u0279\u00f8\u0183\u0279\u01ddss \u00f8\u0279 \u0183\u0279\u0250d\u01dds \u025f\u0279\u00f8\u026f \u028e\u00f8n\u0279 \u0254n\u0279\u0279\u01ddn\u0287 s\u01ddss\u1d09\u00f8n \u028d\u1d09ll b\u01dd l\u00f8s\u0287.", "Any divided discussion topics are divided based on cohort.": "\u023an\u028e d\u1d09\u028c\u1d09d\u01ddd d\u1d09s\u0254nss\u1d09\u00f8n \u0287\u00f8d\u1d09\u0254s \u0250\u0279\u01dd d\u1d09\u028c\u1d09d\u01ddd b\u0250s\u01ddd \u00f8n \u0254\u00f8\u0265\u00f8\u0279\u0287.", "Any divided discussion topics are divided based on enrollment track.": "\u023an\u028e d\u1d09\u028c\u1d09d\u01ddd d\u1d09s\u0254nss\u1d09\u00f8n \u0287\u00f8d\u1d09\u0254s \u0250\u0279\u01dd d\u1d09\u028c\u1d09d\u01ddd b\u0250s\u01ddd \u00f8n \u01ddn\u0279\u00f8ll\u026f\u01ddn\u0287 \u0287\u0279\u0250\u0254\u029e.", + "Application Details": "\u023addl\u1d09\u0254\u0250\u0287\u1d09\u00f8n \u0110\u01dd\u0287\u0250\u1d09ls", "Are you having trouble finding a team to join?": "\u023a\u0279\u01dd \u028e\u00f8n \u0265\u0250\u028c\u1d09n\u0183 \u0287\u0279\u00f8nbl\u01dd \u025f\u1d09nd\u1d09n\u0183 \u0250 \u0287\u01dd\u0250\u026f \u0287\u00f8 \u027e\u00f8\u1d09n?", "Are you sure that you want to leave this session?": "\u023a\u0279\u01dd \u028e\u00f8n sn\u0279\u01dd \u0287\u0265\u0250\u0287 \u028e\u00f8n \u028d\u0250n\u0287 \u0287\u00f8 l\u01dd\u0250\u028c\u01dd \u0287\u0265\u1d09s s\u01ddss\u1d09\u00f8n?", "Are you sure you want to change to a different session?": "\u023a\u0279\u01dd \u028e\u00f8n sn\u0279\u01dd \u028e\u00f8n \u028d\u0250n\u0287 \u0287\u00f8 \u0254\u0265\u0250n\u0183\u01dd \u0287\u00f8 \u0250 d\u1d09\u025f\u025f\u01dd\u0279\u01ddn\u0287 s\u01ddss\u1d09\u00f8n?", @@ -288,7 +288,6 @@ "Automatic transcripts are disabled.": "\u023an\u0287\u00f8\u026f\u0250\u0287\u1d09\u0254 \u0287\u0279\u0250ns\u0254\u0279\u1d09d\u0287s \u0250\u0279\u01dd d\u1d09s\u0250bl\u01ddd.", "Average": "\u023a\u028c\u01dd\u0279\u0250\u0183\u01dd", "Back to sign in": "\u0243\u0250\u0254\u029e \u0287\u00f8 s\u1d09\u0183n \u1d09n", - "Back to {platform} FAQs": "\u0243\u0250\u0254\u029e \u0287\u00f8 {platform} F\u023aQs", "Background color": "\u0243\u0250\u0254\u029e\u0183\u0279\u00f8nnd \u0254\u00f8l\u00f8\u0279", "Basic": "\u0243\u0250s\u1d09\u0254", "Basic Account Information": "\u0243\u0250s\u1d09\u0254 \u023a\u0254\u0254\u00f8nn\u0287 \u0197n\u025f\u00f8\u0279\u026f\u0250\u0287\u1d09\u00f8n", @@ -312,7 +311,6 @@ "Border color": "\u0243\u00f8\u0279d\u01dd\u0279 \u0254\u00f8l\u00f8\u0279", "Bottom": "\u0243\u00f8\u0287\u0287\u00f8\u026f", "Browse": "\u0243\u0279\u00f8\u028ds\u01dd", - "Browse recently launched courses and see what's new in your favorite subjects.": "\u0243\u0279\u00f8\u028ds\u01dd \u0279\u01dd\u0254\u01ddn\u0287l\u028e l\u0250nn\u0254\u0265\u01ddd \u0254\u00f8n\u0279s\u01dds \u0250nd s\u01dd\u01dd \u028d\u0265\u0250\u0287's n\u01dd\u028d \u1d09n \u028e\u00f8n\u0279 \u025f\u0250\u028c\u00f8\u0279\u1d09\u0287\u01dd snb\u027e\u01dd\u0254\u0287s.", "Browse recently launched courses and see what\\'s new in your favorite subjects": "\u0243\u0279\u00f8\u028ds\u01dd \u0279\u01dd\u0254\u01ddn\u0287l\u028e l\u0250nn\u0254\u0265\u01ddd \u0254\u00f8n\u0279s\u01dds \u0250nd s\u01dd\u01dd \u028d\u0265\u0250\u0287\\'s n\u01dd\u028d \u1d09n \u028e\u00f8n\u0279 \u025f\u0250\u028c\u00f8\u0279\u1d09\u0287\u01dd snb\u027e\u01dd\u0254\u0287s", "Browsing": "\u0243\u0279\u00f8\u028ds\u1d09n\u0183", "Bulk Exceptions": "\u0243nl\u029e \u0246x\u0254\u01ddd\u0287\u1d09\u00f8ns", @@ -444,6 +442,7 @@ "Copy": "\u023b\u00f8d\u028e", "Copy Component Location": "\u023b\u00f8d\u028e \u023b\u00f8\u026fd\u00f8n\u01ddn\u0287 \u0141\u00f8\u0254\u0250\u0287\u1d09\u00f8n", "Copy Email To Editor": "\u023b\u00f8d\u028e \u0246\u026f\u0250\u1d09l \u0166\u00f8 \u0246d\u1d09\u0287\u00f8\u0279", + "Copy Unit": "\u023b\u00f8d\u028e \u0244n\u1d09\u0287", "Copy of '{componentDisplayName}'": "\u023b\u00f8d\u028e \u00f8\u025f '{componentDisplayName}'", "Copy row": "\u023b\u00f8d\u028e \u0279\u00f8\u028d", "Copy to Clipboard": "\u023b\u00f8d\u028e \u0287\u00f8 \u023bl\u1d09db\u00f8\u0250\u0279d", @@ -735,7 +734,6 @@ "Explanation": "\u0246xdl\u0250n\u0250\u0287\u1d09\u00f8n", "Explicitly Hiding from Students": "\u0246xdl\u1d09\u0254\u1d09\u0287l\u028e \u0126\u1d09d\u1d09n\u0183 \u025f\u0279\u00f8\u026f S\u0287nd\u01ddn\u0287s", "Explore Programs": "\u0246xdl\u00f8\u0279\u01dd \u2c63\u0279\u00f8\u0183\u0279\u0250\u026fs", - "Explore courses": "\u0246xdl\u00f8\u0279\u01dd \u0254\u00f8n\u0279s\u01dds", "Explore new programs": "\u0246xdl\u00f8\u0279\u01dd n\u01dd\u028d d\u0279\u00f8\u0183\u0279\u0250\u026fs", "Explore subscription options": "\u0246xdl\u00f8\u0279\u01dd snbs\u0254\u0279\u1d09d\u0287\u1d09\u00f8n \u00f8d\u0287\u1d09\u00f8ns", "Explore your course!": "\u0246xdl\u00f8\u0279\u01dd \u028e\u00f8n\u0279 \u0254\u00f8n\u0279s\u01dd!", @@ -1031,6 +1029,7 @@ "Manage Learners": "M\u0250n\u0250\u0183\u01dd \u0141\u01dd\u0250\u0279n\u01dd\u0279s", "Manage Tags": "M\u0250n\u0250\u0183\u01dd \u0166\u0250\u0183s", "Manage my subscription": "M\u0250n\u0250\u0183\u01dd \u026f\u028e snbs\u0254\u0279\u1d09d\u0287\u1d09\u00f8n", + "Manage tags": "M\u0250n\u0250\u0183\u01dd \u0287\u0250\u0183s", "Manual": "M\u0250nn\u0250l", "Mark Exam As Completed": "M\u0250\u0279\u029e \u0246x\u0250\u026f \u023as \u023b\u00f8\u026fdl\u01dd\u0287\u01ddd", "Mark as Answer": "M\u0250\u0279\u029e \u0250s \u023ans\u028d\u01dd\u0279", @@ -1283,6 +1282,7 @@ "Professional Education Verified Certificate": "\u2c63\u0279\u00f8\u025f\u01ddss\u1d09\u00f8n\u0250l \u0246dn\u0254\u0250\u0287\u1d09\u00f8n V\u01dd\u0279\u1d09\u025f\u1d09\u01ddd \u023b\u01dd\u0279\u0287\u1d09\u025f\u1d09\u0254\u0250\u0287\u01dd", "Profile": "\u2c63\u0279\u00f8\u025f\u1d09l\u01dd", "Profile Image": "\u2c63\u0279\u00f8\u025f\u1d09l\u01dd \u0197\u026f\u0250\u0183\u01dd", + "Profile Information": "\u2c63\u0279\u00f8\u025f\u1d09l\u01dd \u0197n\u025f\u00f8\u0279\u026f\u0250\u0287\u1d09\u00f8n", "Profile Visibility:": "\u2c63\u0279\u00f8\u025f\u1d09l\u01dd V\u1d09s\u1d09b\u1d09l\u1d09\u0287\u028e:", "Profile image for {username}": "\u2c63\u0279\u00f8\u025f\u1d09l\u01dd \u1d09\u026f\u0250\u0183\u01dd \u025f\u00f8\u0279 {username}", "Program Record": "\u2c63\u0279\u00f8\u0183\u0279\u0250\u026f \u024c\u01dd\u0254\u00f8\u0279d", @@ -1308,7 +1308,6 @@ "Reason for change:": "\u024c\u01dd\u0250s\u00f8n \u025f\u00f8\u0279 \u0254\u0265\u0250n\u0183\u01dd:", "Receive updates": "\u024c\u01dd\u0254\u01dd\u1d09\u028c\u01dd ndd\u0250\u0287\u01dds", "Recent Activity": "\u024c\u01dd\u0254\u01ddn\u0287 \u023a\u0254\u0287\u1d09\u028c\u1d09\u0287\u028e", - "Recommendations for you": "\u024c\u01dd\u0254\u00f8\u026f\u026f\u01ddnd\u0250\u0287\u1d09\u00f8ns \u025f\u00f8\u0279 \u028e\u00f8n", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "\u024c\u01dd\u0254\u00f8\u026f\u026f\u01ddnd\u01ddd \u1d09\u026f\u0250\u0183\u01dd \u0279\u01dds\u00f8ln\u0287\u1d09\u00f8n \u1d09s {imageResolution}, \u026f\u0250x\u1d09\u026fn\u026f \u1d09\u026f\u0250\u0183\u01dd \u025f\u1d09l\u01dd s\u1d09z\u01dd s\u0265\u00f8nld b\u01dd {maxFileSize} \u0250nd \u025f\u00f8\u0279\u026f\u0250\u0287 \u026fns\u0287 b\u01dd \u00f8n\u01dd \u00f8\u025f {supportedImageFormats}.", "Recover my password": "\u024c\u01dd\u0254\u00f8\u028c\u01dd\u0279 \u026f\u028e d\u0250ss\u028d\u00f8\u0279d", "Recovery Email Address": "\u024c\u01dd\u0254\u00f8\u028c\u01dd\u0279\u028e \u0246\u026f\u0250\u1d09l \u023add\u0279\u01ddss", @@ -1653,7 +1652,7 @@ "The following email addresses and/or usernames are invalid:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u01dd\u026f\u0250\u1d09l \u0250dd\u0279\u01ddss\u01dds \u0250nd/\u00f8\u0279 ns\u01dd\u0279n\u0250\u026f\u01dds \u0250\u0279\u01dd \u1d09n\u028c\u0250l\u1d09d:", "The following errors were generated:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u01dd\u0279\u0279\u00f8\u0279s \u028d\u01dd\u0279\u01dd \u0183\u01ddn\u01dd\u0279\u0250\u0287\u01ddd:", "The following files already exist in this course but don't match the version used by the component you pasted:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u025f\u1d09l\u01dds \u0250l\u0279\u01dd\u0250d\u028e \u01ddx\u1d09s\u0287 \u1d09n \u0287\u0265\u1d09s \u0254\u00f8n\u0279s\u01dd bn\u0287 d\u00f8n'\u0287 \u026f\u0250\u0287\u0254\u0265 \u0287\u0265\u01dd \u028c\u01dd\u0279s\u1d09\u00f8n ns\u01ddd b\u028e \u0287\u0265\u01dd \u0254\u00f8\u026fd\u00f8n\u01ddn\u0287 \u028e\u00f8n d\u0250s\u0287\u01ddd:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u1d09n\u025f\u00f8\u0279\u026f\u0250\u0287\u1d09\u00f8n \u1d09s \u0250l\u0279\u01dd\u0250d\u028e \u0250 d\u0250\u0279\u0287 \u00f8\u025f \u028e\u00f8n\u0279 {platform} d\u0279\u00f8\u025f\u1d09l\u01dd. W\u01dd'\u028c\u01dd \u1d09n\u0254lnd\u01ddd \u1d09\u0287 \u0265\u01dd\u0279\u01dd \u025f\u00f8\u0279 \u028e\u00f8n\u0279 \u0250ddl\u1d09\u0254\u0250\u0287\u1d09\u00f8n.", + "The following information is already a part of your {platform} profile and is required for your application. To edit this information go to ": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u1d09n\u025f\u00f8\u0279\u026f\u0250\u0287\u1d09\u00f8n \u1d09s \u0250l\u0279\u01dd\u0250d\u028e \u0250 d\u0250\u0279\u0287 \u00f8\u025f \u028e\u00f8n\u0279 {platform} d\u0279\u00f8\u025f\u1d09l\u01dd \u0250nd \u1d09s \u0279\u01ddbn\u1d09\u0279\u01ddd \u025f\u00f8\u0279 \u028e\u00f8n\u0279 \u0250ddl\u1d09\u0254\u0250\u0287\u1d09\u00f8n. \u0166\u00f8 \u01ddd\u1d09\u0287 \u0287\u0265\u1d09s \u1d09n\u025f\u00f8\u0279\u026f\u0250\u0287\u1d09\u00f8n \u0183\u00f8 \u0287\u00f8 ", "The following message will be displayed at the bottom of the courseware pages within your course:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u026f\u01ddss\u0250\u0183\u01dd \u028d\u1d09ll b\u01dd d\u1d09sdl\u0250\u028e\u01ddd \u0250\u0287 \u0287\u0265\u01dd b\u00f8\u0287\u0287\u00f8\u026f \u00f8\u025f \u0287\u0265\u01dd \u0254\u00f8n\u0279s\u01dd\u028d\u0250\u0279\u01dd d\u0250\u0183\u01dds \u028d\u1d09\u0287\u0265\u1d09n \u028e\u00f8n\u0279 \u0254\u00f8n\u0279s\u01dd:", "The following options are available for the {license_name} license.": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u00f8d\u0287\u1d09\u00f8ns \u0250\u0279\u01dd \u0250\u028c\u0250\u1d09l\u0250bl\u01dd \u025f\u00f8\u0279 \u0287\u0265\u01dd {license_name} l\u1d09\u0254\u01ddns\u01dd.", "The following required files could not be added to the course:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u0279\u01ddbn\u1d09\u0279\u01ddd \u025f\u1d09l\u01dds \u0254\u00f8nld n\u00f8\u0287 b\u01dd \u0250dd\u01ddd \u0287\u00f8 \u0287\u0265\u01dd \u0254\u00f8n\u0279s\u01dd:", @@ -2237,7 +2236,6 @@ "incorrect": "\u1d09n\u0254\u00f8\u0279\u0279\u01dd\u0254\u0287", "last activity": "l\u0250s\u0287 \u0250\u0254\u0287\u1d09\u028c\u1d09\u0287\u028e", "less than a minute": "l\u01ddss \u0287\u0265\u0250n \u0250 \u026f\u1d09nn\u0287\u01dd", - "loading": "l\u00f8\u0250d\u1d09n\u0183", "marked as answer %(time_ago)s": "\u026f\u0250\u0279\u029e\u01ddd \u0250s \u0250ns\u028d\u01dd\u0279 %(time_ago)s", "marked as answer %(time_ago)s by %(user)s": "\u026f\u0250\u0279\u029e\u01ddd \u0250s \u0250ns\u028d\u01dd\u0279 %(time_ago)s b\u028e %(user)s", "minute": "\u026f\u1d09nn\u0287\u01dd", diff --git a/cms/static/js/i18n/ru/djangojs.js b/cms/static/js/i18n/ru/djangojs.js index c660abfcf825..b36231f6604b 100644 --- a/cms/static/js/i18n/ru/djangojs.js +++ b/cms/static/js/i18n/ru/djangojs.js @@ -138,7 +138,6 @@ "ABCDEFGHIJKLMNOPQRSTUVWXYZ": "\u0410\u0411\u0412\u0413\u0414\u0415\u0416\u0417\u0418\u041a\u041b\u041c\u041d\u041e\u041f\u0420\u0421\u0422\u0423\u0424\u0425\u0426\u0427\u0428\u0429\u042d\u042e\u042f", "Abbreviation": "\u0410\u0431\u0431\u0440\u0435\u0432\u0438\u0430\u0442\u0443\u0440\u0430", "About Me": "\u041e\u0431\u043e \u043c\u043d\u0435", - "About You": "\u041e \u0432\u0430\u0441", "About me": "\u041e \u0441\u0435\u0431\u0435", "Access to some content in this unit is restricted to specific groups of learners": "\u0427\u0430\u0441\u0442\u044c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u044d\u0442\u043e\u0433\u043e \u0431\u043b\u043e\u043a\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0441\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u0435\u0439.", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u042d\u0442\u043e\u0442 {blockType} \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0447\u0430\u0441\u0442\u0438, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0441\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u0435\u0439.", @@ -273,7 +272,6 @@ "Average": "\u0421\u0440\u0435\u0434\u043d\u044f\u044f \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u044c", "Back to Full List": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u043b\u043d\u043e\u043c\u0443 \u0441\u043f\u0438\u0441\u043a\u0443", "Back to sign in": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443", - "Back to {platform} FAQs": "\u041d\u0430\u0437\u0430\u0434 \u043a \u0440\u0430\u0437\u0434\u0435\u043b\u0443 \u00ab\u0412\u043e\u043f\u0440\u043e\u0441\u044b \u0438 \u043e\u0442\u0432\u0435\u0442\u044b\u00bb {platform}", "Background color": "\u0426\u0432\u0435\u0442 \u0444\u043e\u043d\u0430", "Basic": "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435", "Basic Account Information": "\u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", diff --git a/cms/static/js/i18n/tr-tr/djangojs.js b/cms/static/js/i18n/tr-tr/djangojs.js index c60abb5c1d6e..b876d3e30f58 100644 --- a/cms/static/js/i18n/tr-tr/djangojs.js +++ b/cms/static/js/i18n/tr-tr/djangojs.js @@ -175,7 +175,6 @@ "API Secret": "API Secret", "Abbreviation": "K\u0131saltma", "About Me": "Hakk\u0131mda", - "About You": "Hakk\u0131n\u0131zda", "About me": "Hakk\u0131mda", "Access to some content in this unit is restricted to specific groups of learners": "Bu \u00fcnitedeki baz\u0131 i\u00e7eriklere eri\u015fim, belirli \u00f6\u011frenci gruplar\u0131na k\u0131s\u0131tl\u0131d\u0131r", "Access to some content in this {blockType} is restricted to specific groups of learners.": "Bu {blockType} t\u00fcr\u00fcndeki baz\u0131 i\u00e7eriklere eri\u015fim, belirli \u00f6\u011frenci gruplar\u0131na k\u0131s\u0131tl\u0131d\u0131r.", @@ -354,7 +353,6 @@ "Average": "Ortalama", "Back to Full List": "Tam Listeye D\u00f6n", "Back to sign in": "Oturuma geri d\u00f6n", - "Back to {platform} FAQs": "{platform} S\u0131k Sorulan Sorular\u0131'na geri d\u00f6n", "Background color": "Arkaplan rengi", "Basic": "Temel", "Basic Account Information": "Temel Hesap Bilgileri", @@ -379,7 +377,6 @@ "Border color": "Kenarl\u0131k rengi", "Bottom": "Alt", "Browse": "G\u00f6z at", - "Browse recently launched courses and see what's new in your favorite subjects.": "Yeni a\u00e7\u0131lm\u0131\u015f dersleri taray\u0131n ve favori konular\u0131n\u0131z i\u00e7inde yeni ne var g\u00f6r\u00fcnt\u00fcleyin.", "Browse recently launched courses and see what\\'s new in your favorite subjects": "Yeni a\u00e7\u0131lm\u0131\u015f dersleri taray\u0131n ve ilginizi \u00e7eken konularda ne yenilikler var g\u00f6r\u00fcnt\u00fcleyin", "Browsing": "G\u00f6z at\u0131l\u0131yor", "Bulk Exceptions": "Toplu \u0130stisnalar", @@ -513,6 +510,10 @@ "Constrain proportions": "Oranlar\u0131 S\u0131n\u0131rla", "Contact Us": "Bizimle \u0130leti\u015fime Ge\u00e7in", "Contains staff only content": "Sadece personele \u00f6zel i\u00e7erikleri i\u00e7erir", + "Contains {count} group": [ + "{count} grup i\u00e7eriyor", + "{count} grup i\u00e7eriyor" + ], "Content Group ID": "\u0130\u00e7erik Grup ID", "Content Group Name": "\u0130\u00e7erik Grup Ad\u0131", "Content-Specific Discussion Topics": "\u0130\u00e7erik-Odakl\u0131 Tart\u0131\u015fma Ba\u015fl\u0131klar\u0131", @@ -524,6 +525,7 @@ "Copy Component Location": "Bile\u015fen Konumunu Kopyala", "Copy Email To Editor": "Edit\u00f6re e-postay\u0131 kopyala", "Copy Exam Code": "S\u0131nav Kodunu Kopyala", + "Copy of '{componentDisplayName}'": "'{componentDisplayName}' kopyas\u0131", "Copy row": "Sat\u0131r\u0131 kopyala", "Copying": "Kopyal\u0131yor", "Correct failed component": "Ba\u015far\u0131s\u0131z bile\u015feni d\u00fczelt", @@ -657,6 +659,7 @@ "Discussion topics in the course are not divided.": "Dersteki tart\u0131\u015fma ba\u015fl\u0131klar\u0131 b\u00f6l\u00fcnmemi\u015f.", "Discussions are unified; all learners interact with posts from other learners, regardless of the group they are in.": "Tart\u0131\u015fmalar birle\u015ftirilmi\u015f durumda; t\u00fcm \u00f6\u011frenciler i\u00e7inde olduklar\u0131 gruplardan ba\u011f\u0131ms\u0131z olarak, di\u011fer \u00f6\u011frencilerin g\u00f6nderileriyle etkile\u015fime girebilir.", "Discussions enabled": "Tart\u0131\u015fmalar etkinle\u015ftirildi", + "Dismiss": "\u0130ptal", "Display Name": "G\u00f6r\u00fcnen Ad", "Div": "Div", "Divide the selected content-specific discussion topics": "Se\u00e7ili i\u00e7erik-odakl\u0131 tart\u0131\u015fma ba\u015fl\u0131klar\u0131n\u0131 b\u00f6l", @@ -716,6 +719,8 @@ "Email": "E-posta", "Email Address (Sign In)": "E-posta Adresi (Giri\u015f Yap)", "Email address": "E-posta adresi", + "Email cannot be sent to the following users via batch enrollment. They will be allowed to enroll once they register:": "A\u015fa\u011f\u0131daki kullan\u0131c\u0131lara toplu kay\u0131t yoluyla e-posta g\u00f6nderilemez. Sisteme kay\u0131t olduklar\u0131nda derslere kaydolmalar\u0131na izin verilecektir:", + "Email cannot be sent to the following users via batch enrollment. They will be enrolled once they register:": "A\u015fa\u011f\u0131daki kullan\u0131c\u0131lara toplu kay\u0131t yoluyla e-posta g\u00f6nderilemez. Sisteme kay\u0131t olduklar\u0131nda derslere kaydedileceklerdir:", "Emails successfully sent. The following users are no longer enrolled in the course:": "E-postalar ba\u015far\u0131yla g\u00f6nderildi. A\u015fa\u011f\u0131daki kullan\u0131c\u0131lar bu derse art\u0131k kay\u0131tl\u0131 de\u011filler:", "Embed": "G\u00f6m\u00fcl\u00fc", "Emoticons": "\u0130fadeler", @@ -733,6 +738,7 @@ "Ends {end}": "Bitti {end}", "Engage with posts": "G\u00f6nderilerle etkile\u015fimde bulunun", "Enroll Now": "Hemen Kaydol", + "Enroll in a {programName}'s course": "{programName} dersine kaydol", "Enrolled": "Kay\u0131tland\u0131", "Enrolling you in the selected course": "Se\u00e7ilen derse sizi kay\u0131t ediyor", "Enrollment Date": "Kay\u0131t Tarihi", @@ -825,7 +831,6 @@ "Explanation": "A\u00e7\u0131klama", "Explicitly Hiding from Students": "\u00d6\u011frencilerden A\u00e7\u0131k\u00e7a Gizle", "Explore Programs": "Programlar\u0131 Ke\u015ffedin", - "Explore courses": "Dersleri ke\u015ffet", "Explore your course!": "E\u011fitiminizi ke\u015ffedin!", "Failed Proctoring": "Ba\u015far\u0131s\u0131z G\u00f6zetimli S\u0131nav", "Failed to clone rubric": "Dereceli puanlama anahtar\u0131 klonlanamad\u0131", @@ -1190,6 +1195,7 @@ "New Password": "Yeni Parola", "New document": "Yeni belge", "New enrollment mode:": "Yeni kay\u0131t t\u00fcr\u00fc:", + "New file(s) added to Files & Uploads.": "Dosyalar ve Y\u00fcklemeler'e yeni dosya(lar) eklendi.", "New window": "Yeni pencere", "New {component_type}": "Yeni {component_type}", "Next": "Sonraki", @@ -1445,7 +1451,6 @@ "Reason for change:": "De\u011fi\u015fim nedeni:", "Receive updates": "G\u00fcncellemeleri al", "Recent Activity": "Son Etkinlik", - "Recommendations for you": "Sizin i\u00e7in tavsiyeler", "Recommended image resolution is {imageResolution}, maximum image file size should be {maxFileSize} and format must be one of {supportedImageFormats}.": "Tavsiye edilen g\u00f6rsel \u00e7\u00f6z\u00fcn\u00fcrl\u00fc\u011f\u00fc {imageResolution}, maksimum g\u00f6rsel dosya boyutu {maxFileSize} ve {supportedImageFormats} formatlar\u0131ndan birisi olmal\u0131d\u0131r.", "Recover my password": "Parolam\u0131 kurtar", "Recovery Email Address": "Kurtarma E-posta Adresi", @@ -1642,6 +1647,7 @@ "Skip": "Atla", "Social Media Links": "Sosyal Medya Ba\u011flant\u0131lar\u0131", "Some Rights Reserved": "Baz\u0131 Haklar\u0131 Sakl\u0131d\u0131r", + "Some errors occurred": "Baz\u0131 hatalar ger\u00e7ekle\u015fti", "Some images in this post have been omitted": "Bu g\u00f6nderideki baz\u0131 g\u00f6rseller atland\u0131", "Something went wrong changing this enrollment. Please try again.": "Bu kayd\u0131 de\u011fi\u015ftirirken bir \u015feyler yanl\u0131\u015f gitti. L\u00fctfen tekrar deneyin.", "Something went wrong. Please try again later.": "Bir \u015feyler yanl\u0131\u015f gitti. L\u00fctfen daha sonra tekrar deneyin.", @@ -1703,6 +1709,7 @@ "Submit enrollment change": "Kay\u0131t de\u011fi\u015fimini g\u00f6nder", "Submitted": "Girildi", "Subscript": "Alt simge", + "Subscription trial expires in less than 24 hours": "Abonelik deneme s\u00fcresi 24 saat i\u00e7inde sona eriyor", "Subscription trial expires in {remainingDays} day": [ "Abonelik deneme s\u00fcresi {remainingDays} g\u00fcn i\u00e7inde sona eriyor", "Abonelik deneme s\u00fcresi {remainingDays} g\u00fcn i\u00e7inde sona eriyor" @@ -1805,9 +1812,10 @@ "The following email addresses and/or usernames are invalid:": "A\u015fa\u011f\u0131daki e-posta adresleri veya kullan\u0131c\u0131 adlar\u0131 ge\u00e7ersizdir.", "The following errors were generated:": "A\u015fa\u011f\u0131daki hatalar olu\u015ftu:", "The following file types are not allowed: ": "A\u015fa\u011f\u0131daki dosya t\u00fcr\u00fcne izin verilmiyor:", - "The following information is already a part of your {platform} profile. We've included it here for your application.": "A\u015fa\u011f\u0131daki bilgiler {platform} profilinizde zaten mevcut. Ba\u015fvurunuz i\u00e7in buraya ekledik.", "The following message will be displayed at the bottom of the courseware pages within your course:": "A\u015fa\u011f\u0131daki ileti e\u011fitiminizdeki ders yaz\u0131l\u0131m sayfas\u0131n\u0131n alt\u0131nda g\u00f6r\u00fcnt\u00fclenecektir.", "The following options are available for the {license_name} license.": "A\u015fa\u011f\u0131daki se\u00e7enekler {license_name} lisans\u0131 i\u00e7in ge\u00e7erlidir. ", + "The following required files could not be added to the course:": "A\u015fa\u011f\u0131daki gerekli dosyalar derse eklenemedi:", + "The following required files were imported to this course:": "A\u015fa\u011f\u0131daki gerekli dosyalar bu derse aktar\u0131ld\u0131:", "The following users are no longer enrolled in the course:": "A\u015fa\u011f\u0131daki kullan\u0131c\u0131lar art\u0131k bu derse kay\u0131tl\u0131 de\u011filler:", "The following warnings were generated:": "A\u015fa\u011f\u0131daki uyar\u0131lar olu\u015fturuldu:", "The general category for this type of assignment, for example, Homework or Midterm Exam. This name is visible to learners.": "Bu g\u00f6rev t\u00fcr\u00fc i\u00e7in belirlenen genel kategori, \u00f6rne\u011fin, Ev \u00d6devi veya Ara S\u0131nav gibi. Bu isim t\u00fcm \u00f6\u011frenciler taraf\u0131ndan g\u00f6r\u00fcnt\u00fclenecek.", @@ -1915,6 +1923,7 @@ "This learner will be removed from the team,allowing another learner to take the available spot.": "Bu \u00f6\u011frenci tak\u0131mdan \u00e7\u0131kar\u0131lacak ve ba\u015fka bir \u00f6\u011frencinin uygun yeri almas\u0131na izin verilecektir.", "This link will open in a modal window": "Bu ba\u011flant\u0131 yeni bir kip penceresinde a\u00e7\u0131lacak", "This link will open in a new browser window/tab": "Bu ba\u011flant\u0131 taray\u0131c\u0131da yeni bir pencere veya sekmede a\u00e7\u0131lacak", + "This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.": "Bu durum sunucudaki bir hatadan veya \u0130nternet ba\u011flant\u0131n\u0131zdan kaynaklanm\u0131\u015f olabilir. Sayfay\u0131 yeniden y\u00fcklemeyi deneyin veya \u00e7evrimi\u00e7i oldu\u011funuza emin olun.", "This page contains information about orders that you have placed with {platform_name}.": "Bu sayfa {platform_name} i\u00e7inde verdi\u011finiz sipari\u015fleriniz hakk\u0131nda bilgileri i\u00e7erir.", "This post could not be closed. Refresh the page and try again.": "Bu g\u00f6nderi kapat\u0131lamaz. Sayfay\u0131 yenileyin ve tekrar deneyin.", "This post could not be flagged for abuse. Refresh the page and try again.": "Bu g\u00f6nderiye taciz i\u015faretlemesi yap\u0131lamaz. Sayfay\u0131 yenileyin ve tekrar deneyin.", @@ -2102,6 +2111,10 @@ "Use your webcam to take a photo of your ID.": "Kimli\u011finizin foto\u011fraf\u0131n\u0131 \u00e7ekmek i\u00e7in web kameran\u0131z\u0131 kullan\u0131n.", "Use your webcam to take a photo of your face. We will match this photo with the photo on your ID.": "Y\u00fcz\u00fcn\u00fcz\u00fcn foto\u011fraf\u0131n\u0131 \u00e7ekmek i\u00e7in web kameran\u0131z\u0131 kullan\u0131n. Biz bu foto\u011fraf\u0131n\u0131z\u0131 kimli\u011finiz \u00fczerindeki foto\u011fraf ile e\u015fle\u015ftirece\u011fiz.", "Used": "Kullan\u0131ld\u0131", + "Used in {count} location": [ + "{count} konumda kullan\u0131l\u0131yor", + "{count} konumda kullan\u0131l\u0131yor" + ], "User Email": "Kullan\u0131c\u0131 E-postas\u0131", "User lookup failed": "Kullan\u0131c\u0131 ararken hata", "Username": "Kullan\u0131c\u0131 ad\u0131", @@ -2150,6 +2163,7 @@ "View and grade responses": "Cevaplar\u0131 g\u00f6r\u00fcnt\u00fcle ve notland\u0131r", "View child items": "Alt \u00f6\u011feleri g\u00f6r\u00fcnt\u00fcleyin", "View discussion": "Tart\u0131\u015fmay\u0131 g\u00f6r\u00fcnt\u00fcle", + "View files": "Dosyalar\u0131 g\u00f6r\u00fcnt\u00fcle", "View my exam": "S\u0131nav\u0131m\u0131 g\u00f6r\u00fcnt\u00fcle", "View program": "Program\u0131 g\u00f6ster", "View {span_start} {team_name} {span_end}": "G\u00f6r\u00fcnt\u00fcle {span_start} {team_name} {span_end}", @@ -2248,6 +2262,7 @@ "You did not submit the required files: {requiredFiles}.": "Gerekli dosyalar\u0131 g\u00f6ndermediniz: {requiredFiles}.", "You don't seem to have Flash installed. Get Flash to continue your verification.": "Flash'\u0131 y\u00fcklememi\u015f g\u00f6z\u00fck\u00fcyorsunuz. Do\u011frulamaya devam etmeniz i\u00e7in Flash'\u0131 y\u00fckleyin.", "You don't seem to have a webcam connected.": "Ba\u011fl\u0131 bir web kameran\u0131z olmad\u0131\u011f\u0131 g\u00f6r\u00fcnmekte.", + "You have access to the {enterpriseName} dashboard": "{enterpriseName} paneline eri\u015fiminiz var", "You have added a criterion. You will need to select an option for the criterion in the Learner Training step. To do this, click the Assessment Steps tab.": "Bir \u00f6l\u00e7\u00fct eklediniz. \u00d6\u011frenci E\u011fitimi ad\u0131m\u0131nda, \u00f6l\u00e7\u00fct i\u00e7in bir se\u00e7enek belirlemelisiniz. Bunu yapmak i\u00e7in, De\u011ferlendirme Ad\u0131mlar\u0131 sekmesine t\u0131klay\u0131n\u0131z.", "You have already verified your ID!": "Kimli\u011finizi \u00e7oktan do\u011frulad\u0131n\u0131z!", "You have an active subscription to the {programName} program but are not enrolled in any courses. Enroll in a remaining course and enjoy verified access.": "{programName} program\u0131na etkin bir aboneli\u011finiz var ancak herhangi bir derse kay\u0131tl\u0131 de\u011filsiniz. Mevcut bir derse kaydolun ve do\u011frulanm\u0131\u015f eri\u015fimin keyfini \u00e7\u0131kar\u0131n.", @@ -2275,6 +2290,7 @@ "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.": "Hesap silme sonras\u0131nda, onaylanm\u0131\u015f sertifikalara ve MicroMasters sertifikalar\u0131 gibi di\u011fer program kimlik bilgilerine de eri\u015femezsiniz. Silme i\u015fleminden \u00f6nce bu kay\u0131tlar\u0131n\u0131z\u0131n bir kopyas\u0131n\u0131 \u00e7\u0131karmak i\u00e7in l\u00fctfen {htmlStart}bir sertifikan\u0131n \u00e7\u0131kt\u0131s\u0131n\u0131 almak ya da indirmek{htmlEnd} \u00f6nergelerini takip edin.", "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. ": "G\u00f6r\u00fcnt\u00fc yakalama s\u00fcrecini yard\u0131m almadan tamamlayabilirsiniz, ancak kameran\u0131n do\u011fru konumland\u0131r\u0131lmas\u0131 i\u00e7in birka\u00e7 g\u00f6nderim denemesi gerekebilir. Optimum kamera konumu her bilgisayara g\u00f6re de\u011fi\u015fir, ancak genellikle bir y\u00fcz foto\u011fraf\u0131 i\u00e7in en iyi konum, kafan\u0131z ekran\u0131n\u0131za g\u00f6re ortalanm\u0131\u015f olarak kameradan 12-18 in\u00e7 (30-45 santimetre) uzakl\u0131kt\u0131r. ", "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. ": "G\u00f6r\u00fcnt\u00fc yakalama s\u00fcrecini yard\u0131m almadan tamamlayabilirsiniz, ancak kameran\u0131n do\u011fru konumland\u0131r\u0131lmas\u0131 i\u00e7in birka\u00e7 g\u00f6nderim denemesi gerekebilir. Optimum kamera konumu her bilgisayara g\u00f6re de\u011fi\u015fir, ancak genellikle bir kimlik kart\u0131n\u0131n foto\u011fraf\u0131 i\u00e7in en iyi konum, kimlik kart\u0131 kameraya g\u00f6re ortalanm\u0131\u015f olarak kameradan 8-12 in\u00e7 (20-30 santimetre) uzakl\u0131kt\u0131r. ", + "You may need to update a file(s) manually": "Dosyay\u0131/dosyalar\u0131 elle g\u00fcncellemeniz gerekebilir", "You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}": "T\u00fcm profili payla\u015fmak i\u00e7in 13 ya\u015f\u0131ndan b\u00fcy\u00fck olman\u0131z gerekmektedir. E\u011fer 13 ya\u015f\u0131ndan b\u00fcy\u00fckseniz, {account_settings_page_link} sayfas\u0131nda do\u011fum y\u0131l\u0131n\u0131z\u0131 belirtti\u011finizden emin olun.", "You must enter a valid email address in order to add a new team member": "Yeni bir ekip \u00fcyesi eklemek i\u00e7in ge\u00e7erli bir e-posta adresi girmelisiniz.", "You must have at least one undroppable <%- types %> assignment.": "En az bir adet b\u0131rak\u0131lamaz <%- types %> ataman\u0131z olmal\u0131d\u0131r.", @@ -2343,6 +2359,11 @@ "Your upload of '{file}' succeeded.": "'{file}' y\u00fcklemeniz ba\u015far\u0131l\u0131.", "Your verification status is good until {verificationGoodUntil}.": "{verificationGoodUntil} a\u015famas\u0131na dek do\u011frulama durumunuz iyi.", "Your video uploads are not complete.": "Video y\u00fcklemeleriniz tamamlanmad\u0131.", + "Your {programName} trial will expire at {trialEndTime} on {trialEndDate} and the card on file will be charged {subscriptionPrice}.": "{programName} deneme s\u00fcr\u00fc\u015f\u00fcn\u00fcz {trialEndDate} tarihinde {trialEndTime} saati itibariyle sona erecek ve dosyadaki karttan {subscriptionPrice} tahsil edilecektir.", + "Your {programName} trial will expire in {remainingDays} day at {trialEndTime} on {trialEndDate} and the card on file will be charged {subscriptionPrice}.": [ + "{programName} deneme s\u00fcr\u00fc\u015f\u00fcn\u00fcz {remainingDays} g\u00fcn sonra {trialEndDate} tarihinde {trialEndTime} saati itibariyle sona erecek ve dosyadaki karttan {subscriptionPrice} tahsil edilecektir.", + "{programName} deneme s\u00fcr\u00fc\u015f\u00fcn\u00fcz {remainingDays} g\u00fcn sonra {trialEndDate} tarihinde {trialEndTime} saati itibariyle sona erecek ve dosyadaki karttan {subscriptionPrice} tahsil edilecektir." + ], "Your {program} Certificate": "{program} Sertifikan\u0131z", "Yourself": "Kendiniz", "Zoom In": "B\u00fcy\u00fct", @@ -2416,7 +2437,6 @@ "internally reviewed": "dahili olarak g\u00f6zden ge\u00e7irildi", "last activity": "son faaliyet", "less than a minute": "bir dakikadan az", - "loading": "y\u00fckleniyor", "marked as answer %(time_ago)s": "%(time_ago)s \u00f6nce cevap olarak i\u015faretlendi", "marked as answer %(time_ago)s by %(user)s": "%(user)s taraf\u0131ndan cevap olarak i\u015faretlendi %(time_ago)s", "minute": "dakika", diff --git a/cms/static/js/i18n/zh-cn/djangojs.js b/cms/static/js/i18n/zh-cn/djangojs.js index 9b17865c658f..eefa3ad21ad8 100644 --- a/cms/static/js/i18n/zh-cn/djangojs.js +++ b/cms/static/js/i18n/zh-cn/djangojs.js @@ -55,6 +55,8 @@ " to complete and submit the exam.": "\u6765\u5b8c\u6210\u5e76\u63d0\u4ea4\u8003\u8bd5\u3002", "${listPrice}": "${listPrice}", "%(cohort_name)s (%(user_count)s)": "%(cohort_name)s (%(user_count)s)", + "%(comments_count)s %(span_sr_open)scomments %(span_close)s": "%(comments_count)s %(span_sr_open)s\u8bc4\u8bba %(span_close)s", + "%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s unread comments)%(span_close)s": "%(comments_count)s %(span_sr_open)s\u8bc4\u8bba (%(unread_comments_count)s \u672a\u8bfb\u8bc4\u8bba)%(span_close)s", "%(errorCount)s error found in form.": [ "\u8868\u683c\u4e2d\u53d1\u73b0 %(errorCount)s \u4e2a\u9519\u8bef\u3002" ], @@ -88,6 +90,7 @@ "%s from now": "\u8ddd\u73b0\u5728:%s", "(Add signatories for a certificate)": "(\u6dfb\u52a0\u8bc1\u4e66\u4e2d\u7684\u7b7e\u7f72\u65b9)", "(Caption will be displayed when you start playing the video.)": "(\u5f00\u59cb\u64ad\u653e\u89c6\u9891\u65f6\u5c06\u663e\u793a\u5b57\u5e55)", + "(Community TA)": "(\u793e\u533a\u52a9\u6559)", "(Optional)": "(\u975e\u5fc5\u586b)", "(Read-only)": "\uff08\u53ea\u8bfb\uff09", "(Required Field)": "(\u5fc5\u586b\u5b57\u6bb5)", @@ -95,6 +98,7 @@ "(Self-paced) Ends {end}": "(\u81ea\u4e3b\u6a21\u5f0f) \u7ed3\u675f {end}", "(Self-paced) Started {start}": "(\u81ea\u4e3b\u6a21\u5f0f) \u5f00\u59cb {start}", "(Self-paced) Starts {start}": "(\u81ea\u4e3b\u6a21\u5f0f) \u5f00\u59cb {start}", + "(Staff)": "(\u5458\u5de5)", "(contains %(student_count)s student)": [ "\uff08\u5305\u62ec %(student_count)s \u4e2a\u5b66\u751f\uff09" ], @@ -115,7 +119,6 @@ "API Secret": "API Secret", "Abbreviation": "\u7f29\u5199", "About Me": "\u4e2a\u4eba\u8d44\u6599", - "About You": "\u5173\u4e8e\u60a8", "About me": "\u4e2a\u4eba\u8d44\u6599", "Access to some content in this unit is restricted to specific groups of learners": "\u4ec5\u9650\u6307\u5b9a\u5206\u7ec4\u5b66\u5458\u53ef\u8bbf\u95ee\u6b64\u5355\u5143\u7684\u90e8\u5206\u5185\u5bb9", "Access to some content in this {blockType} is restricted to specific groups of learners.": "\u6b64{blockType}\u7684\u67d0\u4e9b\u5185\u5bb9\u4ec5\u9650\u6307\u5b9a\u5206\u7ec4\u5b66\u5458\u53ef\u8bbf\u95ee\u3002", @@ -148,7 +151,12 @@ "Add URLs for additional versions": "\u6dfb\u52a0\u5176\u4ed6\u7248\u672c\u7684URL", "Add a Chapter": "\u6dfb\u52a0\u4e00\u7ae0", "Add a New Cohort": "\u6dfb\u52a0\u65b0\u7fa4\u7ec4", + "Add a Post": "\u6dfb\u52a0\u5e16\u5b50", + "Add a Response": "\u6dfb\u52a0\u56de\u590d", + "Add a clear and descriptive title to encourage participation. (Required)": "\u6dfb\u52a0\u4e00\u4e2a\u6e05\u6670\u5e76\u4e14\u5177\u6709\u63cf\u8ff0\u6027\u7684\u6807\u9898\u6765\u9f13\u52b1\u53c2\u4e0e (\u5fc5\u586b\u9879)", + "Add a comment": "\u6dfb\u52a0\u8bc4\u8bba", "Add a learning outcome here": "\u5728\u8fd9\u91cc\u6dfb\u52a0\u5b66\u4e60\u6210\u679c", + "Add a response:": "\u6dfb\u52a0\u4e00\u6761\u56de\u590d\uff1a", "Add another group": "\u6dfb\u52a0\u53e6\u4e00\u4e2a\u7ec4", "Add language": "\u6dfb\u52a0\u8bed\u8a00", "Add learners to this cohort": "\u6dfb\u52a0\u5b66\u5458\u81f3\u6b64\u7fa4\u7ec4", @@ -158,6 +166,7 @@ "Add your first content group": "\u6dfb\u52a0\u9996\u4e2a\u5185\u5bb9\u7ec4", "Add your first group configuration": "\u6dfb\u52a0\u9996\u4e2a\u7ec4\u914d\u7f6e", "Add your first textbook": "\u6dfb\u52a0\u7b2c\u4e00\u672c\u8bfe\u672c", + "Add your post to a relevant topic to help others find it. (Required)": "\u628a\u60a8\u7684\u5e16\u5b50\u53d1\u5e03\u5230\u76f8\u5e94\u7684\u4e3b\u9898\u4ee5\u5e2e\u52a9\u522b\u4eba\u627e\u5230\u5b83 (\u5fc5\u586b\u9879)", "Add {role} Access": "\u6dfb\u52a0{role}\u8bbf\u95ee\u6743\u9650", "Adding": "\u6b63\u5728\u6dfb\u52a0", "Adding the selected course to your cart": "\u6b63\u5728\u5c06\u60a8\u6240\u9009\u7684\u8bfe\u7a0b\u6dfb\u52a0\u5230\u60a8\u7684\u8d2d\u7269\u8f66", @@ -178,6 +187,7 @@ "Align left": "\u5de6\u5bf9\u9f50", "Align right": "\u53f3\u5bf9\u9f50", "Alignment": "\u5bf9\u9f50", + "All Groups": "\u6240\u6709\u5206\u7ec4", "All Learners and Staff": "\u6240\u6709\u5b66\u4e60\u8005\u4e0e\u6559\u5458", "All Posts": "\u6240\u6709\u8ba8\u8bba\u5e16", "All Rights Reserved": "\u4fdd\u7559\u6240\u6709\u6743\u5229", @@ -277,7 +287,6 @@ "Average": "\u97f3\u91cf\u4e2d\u7b49", "Back to Full List": "\u8fd4\u56de\u5b8c\u6574\u5217\u8868", "Back to sign in": "\u8fd4\u56de\u767b\u5f55", - "Back to {platform} FAQs": "\u8fd4\u56de\u81f3 {platform} \u5e38\u89c1\u95ee\u9898\u89e3\u7b54", "Background color": "\u80cc\u666f\u989c\u8272", "Basic": "\u57fa\u672c", "Basic Account Information": "\u57fa\u672c\u8d26\u53f7\u4fe1\u606f", @@ -301,7 +310,6 @@ "Border color": "\u8fb9\u6846\u8272", "Bottom": "\u5e95\u7aef", "Browse": "\u6d4f\u89c8", - "Browse recently launched courses and see what's new in your favorite subjects.": "\u6d4f\u89c8\u6700\u65b0\u5f00\u529e\u7684\u8bfe\u7a0b\uff0c\u770b\u770b\u60a8\u6700\u559c\u6b22\u7684\u79d1\u76ee\u4e2d\u6709\u4e86\u4ec0\u4e48\u65b0\u5185\u5bb9", "Browse recently launched courses and see what\\'s new in your favorite subjects": "\u6d4f\u89c8\u6700\u65b0\u4e0a\u7ebf\u7684\u8bfe\u7a0b\u5e76\u67e5\u770b\u60a8\u6700\u559c\u7231\u79d1\u76ee\u7684\u66f4\u65b0\u60c5\u51b5", "Browsing": "\u6d4f\u89c8", "Bulk Exceptions": "\u6279\u91cf\u7279\u6b8a\u5904\u7406", @@ -389,6 +397,7 @@ "Click to edit": "\u70b9\u51fb\u4ee5\u7f16\u8f91", "Close": "\u5173\u95ed", "Close Calculator": "\u5173\u95ed\u8ba1\u7b97\u5668", + "Closed": "\u5df2\u5173\u95ed", "Code": "\u4ee3\u7801", "Code Sample (Ctrl+K)": "\u4ee3\u7801\u793a\u4f8b(Ctrl+K)", "Code block": "\u4ee3\u7801\u5757", @@ -407,6 +416,7 @@ "Coming Soon": "\u5373\u5c06\u4e0a\u7ebf", "Commentary": "\u8bc4\u6ce8", "Common Problem Types": "\u5e38\u89c1\u95ee\u9898\u7c7b\u578b", + "Community TA": "\u793e\u533a\u52a9\u6559", "Complete courses on your schedule to ensure you stand out in your field!": "\u6309\u65f6\u5b8c\u6210\u8bfe\u7a0b\u5b66\u4e60\uff0c\u6bd4\u522b\u4eba\u5148\u884c\u4e00\u6b65\uff01", "Completed": "\u5df2\u7ecf\u5b8c\u6210", "Component": "\u7ec4\u4ef6", @@ -484,6 +494,7 @@ "Create account using %(providerName)s.": "\u4f7f\u7528 %(providerName)s \u521b\u5efa\u8d26\u53f7\u3002", "Create an Account": "\u6ce8\u518c", "Create an Account.": "\u6ce8\u518c\u8d26\u53f7", + "Create an account": "\u521b\u5efa\u8d26\u6237", "Create an account using": "\u4f7f\u7528\u4ee5\u4e0b\u65b9\u5f0f\u521b\u5efa\u8d26\u53f7", "Create team.": "\u521b\u5efa\u56e2\u961f\u3002", "Created": "\u521b\u5efa", @@ -544,11 +555,14 @@ "Description": "\u63cf\u8ff0", "Description of the certificate": "\u8ba4\u8bc1\u63cf\u8ff0", "Details": "\u7ec6\u8282", + "Device with Camera": "\u5e26\u6444\u50cf\u5934\u7684\u8bbe\u5907", "Dimensions": "\u5c3a\u5bf8", "Disc": "\u5b9e\u5fc3\u5706", "Discard Changes": "\u653e\u5f03\u66f4\u6539", "Discarding Changes": "\u6b63\u5728\u653e\u5f03\u66f4\u6539", + "Discussion": "\u8ba8\u8bba", "Discussion Home": "\u8ba8\u8bba\u533a", + "Discussion admins, moderators, and TAs can make their posts visible to all students or specify a single group.": "\u8bba\u575b\u7ba1\u7406\u5458\u3001\u7248\u4e3b\u548c\u52a9\u6559\u53ef\u8bbe\u7f6e\u5176\u5e16\u5b50\u4e3a\u6240\u6709\u4eba\u53ef\u89c1\u6216\u4ec5\u7279\u5b9a\u5206\u7ec4\u53ef\u89c1\u3002", "Discussion topics in the course are not divided.": "\u4e0d\u533a\u5206\u8bfe\u7a0b\u4e2d\u7684\u8ba8\u8bba\u4e3b\u9898\u3002", "Discussions are unified; all learners interact with posts from other learners, regardless of the group they are in.": "\u8ba8\u8bba\u533a\u662f\u7edf\u4e00\u6807\u51c6\u7684\uff0c\u5b66\u5458\u53ef\u4ee5\u4e0e\u6240\u6709\u5176\u4ed6\u5c0f\u7ec4\u5b66\u5458\u7684\u5e16\u5b50\u8fdb\u884c\u4e92\u52a8\u3002", "Dismiss": "\u89e3\u6563", @@ -566,6 +580,7 @@ "Donate": "\u6350\u732e", "Double-check that your webcam is connected and working to continue.": "\u7ee7\u7eed\u524d\u8bf7\u518d\u6b21\u786e\u8ba4\u60a8\u7684\u6444\u50cf\u5934\u5df2\u7ecf\u8fde\u63a5\u5e76\u4e14\u53ef\u4ee5\u6b63\u5e38\u4f7f\u7528\u3002", "Download": "\u4e0b\u8f7d", + "Download Memberships": "\u4e0b\u8f7d\u4f1a\u5458", "Download Software Clicked": "\u5df2\u70b9\u51fb\u4e0b\u8f7d\u8f6f\u4ef6", "Download Transcript for Editing": "\u4e0b\u8f7d\u5b57\u5e55\u8fdb\u884c\u7f16\u8f91", "Download available encodings (.csv)": "\u4e0b\u8f7d\u53ef\u7528\u7684\u7f16\u7801\u65b9\u5f0f(.csv)", @@ -597,8 +612,12 @@ "Edit Title": "\u7f16\u8f91\u6807\u9898", "Edit Your Name": "\u7f16\u8f91\u60a8\u7684\u540d\u5b57", "Edit this certificate?": "\u662f\u5426\u7f16\u8f91\u6b64\u8bc1\u4e66\uff1f", + "Edit your post below.": "\u7f16\u8f91\u5e16\u5b50", "Editable": "\u53ef\u7f16\u8f91", "Editing access for: {title}": "\u6b63\u5728\u7f16\u8f91\u5bf9\u4e8e{title}\u7684\u8bbf\u95ee\u6743\u9650", + "Editing comment": "\u7f16\u8f91\u8bc4\u8bba", + "Editing post": "\u7f16\u8f91\u8ba8\u8bba\u5e16", + "Editing response": "\u7f16\u8f91\u56de\u590d", "Editing: {title}": "\u6b63\u5728\u7f16\u8f91\uff1a{title}", "Editor": "\u7f16\u8f91\u5668", "Education Completed": "\u6559\u80b2\u7a0b\u5ea6", @@ -616,6 +635,7 @@ "Encoding": "\u7f16\u7801", "End My Exam": "\u7ed3\u675f\u6211\u7684\u8003\u8bd5", "End of transcript. Skip to the start.": "\u5b57\u5e55\u7ed3\u5c3e\u3002\u8df3\u8f6c\u81f3\u5f00\u59cb\u3002", + "Endorse": "\u652f\u6301", "Ends {end}": "\u7ed3\u675f{end}", "Engage with posts": "\u53c2\u4e0e\u8ba8\u8bba", "Enroll Now": "\u73b0\u5728\u9009\u8bfe", @@ -669,6 +689,7 @@ "Error getting the number of ungraded responses": "\u83b7\u53d6\u672a\u8bc4\u5206\u56de\u590d\u6570\u91cf\u51fa\u73b0\u9519\u8bef", "Error importing course": "\u5bfc\u5165\u8bfe\u7a0b\u65f6\u51fa\u9519", "Error listing task history for this student and problem.": "\u663e\u793a\u6b64\u5b66\u751f\u4e0e\u95ee\u9898\u7684\u4efb\u52a1\u5386\u53f2\u65f6\u53d1\u751f\u9519\u8bef\u3002", + "Error posting your message.": "\u53d1\u5e03\u6d88\u606f\u65f6\u53d1\u751f\u9519\u8bef\u3002", "Error removing user": "\u5220\u9664\u7528\u6237\u8fc7\u7a0b\u4e2d\u51fa\u73b0\u9519\u8bef", "Error resetting entrance exam attempts for student '{student_id}'. Make sure student identifier is correct.": "\u91cd\u7f6e\u5b66\u751f'{student_id}'\u7684\u5165\u5b66\u8003\u8bd5\u5c1d\u8bd5\u6b21\u6570\u65f6\u51fa\u9519\u4e86\uff0c\u8bf7\u786e\u8ba4\u5b66\u751f\u7f16\u53f7\u65e0\u8bef\u3002", "Error retrieving grading configuration.": "\u53d6\u5f97\u8bc4\u5206\u6807\u51c6\u65f6\u9519\u8bef\u3002", @@ -703,7 +724,6 @@ "Explanation": "\u89e3\u91ca", "Explicitly Hiding from Students": "\u660e\u786e\u5bf9\u5b66\u751f\u9690\u85cf", "Explore Programs": "\u641c\u7d22\u8bfe\u7a0b", - "Explore courses": "\u63a2\u7d22\u8bfe\u7a0b", "Explore your course!": "\u63a2\u7d22\u60a8\u7684\u8bfe\u7a0b\uff01", "Failed Proctoring": "\u672a\u901a\u8fc7\u76d1\u8003", "Failed to delete student state for user.": "\u5220\u9664\u7528\u6237\u7684\u5b66\u751f\u72b6\u6001\u5931\u8d25\u3002", @@ -734,7 +754,9 @@ "Find previous": "\u67e5\u627e\u4e0a\u4e00\u4e2a", "Finish": "\u5b8c\u6210", "First time here?": "\u521d\u6b21\u4f7f\u7528\uff1f", + "Follow": "\u5173\u6ce8", "Follow or unfollow posts": "\u5173\u6ce8\u6216\u53d6\u6d88\u5173\u6ce8\u53d1\u5e16", + "Following": "\u5173\u6ce8", "Font Family": "\u5b57\u4f53", "Font Sizes": "\u5b57\u53f7", "Footer": "\u811a\u6ce8", @@ -960,6 +982,7 @@ "Live view of webcam": "\u6444\u50cf\u5934\u7684\u5b9e\u65f6\u753b\u9762", "Load Another File": "\u52a0\u8f7d\u5176\u4ed6\u6587\u4ef6", "Load all responses": "\u8f7d\u5165\u6240\u6709\u7684\u56de\u590d", + "Load more": "\u8f7d\u5165\u66f4\u591a", "Load next {numResponses} responses": "\u52a0\u8f7d\u63a5\u4e0b\u6765\u7684{numResponses}\u4e2a\u56de\u590d", "Load next {num_items} result": [ "\u7ee7\u7eed\u52a0\u8f7d {num_items}\u4e2a\u7ed3\u679c" @@ -991,6 +1014,7 @@ "Manage Learners": "\u7ba1\u7406\u5b66\u5458", "Manual": "\u624b\u52a8", "Mark Exam As Completed": "\u6807\u8bb0\u8003\u8bd5\u5b8c\u6210", + "Mark as Answer": "\u6807\u8bb0\u4f5c\u4e3a\u7b54\u6848", "Mark enrollment code as unused": "\u6807\u8bb0\u9009\u8bfe\u7801\u4e3a\u5c1a\u672a\u4f7f\u7528\u7684", "Markdown Editing Help": "Markdown\u7f16\u8f91\u5e2e\u52a9", "Match case": "\u5339\u914d\u5927\u5c0f\u5199", @@ -1026,9 +1050,11 @@ "Name or short description of the configuration": "\u8be5\u914d\u7f6e\u7684\u540d\u79f0\u6216\u7b80\u77ed\u63cf\u8ff0", "Navigate up": "\u5411\u4e0a\u5bfc\u822a", "Need help logging in?": "\u767b\u5f55\u65f6\u9700\u8981\u5e2e\u52a9\uff1f", + "Need help signing in?": "\u9700\u8981\u5e2e\u52a9\u767b\u5f55\uff1f", "Needs verified certificate ": "\u9700\u8981\u5df2\u8ba4\u8bc1\u8bc1\u4e66", "Never published": "\u4ece\u672a\u53d1\u5e03\u8fc7", "Never show assessment results": "\u4e00\u76f4\u9690\u85cf\u8bc4\u5206\u7ed3\u679c", + "New": "\u65b0\u5efa", "New %(item_type)s": "\u65b0\u5efa%(item_type)s", "New Address": "\u65b0\u5730\u5740", "New Password": "\u65b0\u5bc6\u7801", @@ -1090,6 +1116,7 @@ "Only <%- fileTypes %> files can be uploaded. Please select a file ending in <%- (fileExtensions) %> to upload.": "\u53ea\u6709<%- fileTypes %>\u7c7b\u578b\u7684\u6587\u4ef6\u80fd\u591f\u88ab\u4e0a\u4f20\u3002 \u8bf7\u9009\u62e9\u4e00\u4e2a\u7ed3\u5c3e\u4e3a<%- (fileExtensions) %> \u7684\u6587\u4ef6\u4e0a\u4f20\u3002", "Only properly formatted .csv files will be accepted.": "\u53ea\u6709\u6807\u51c6\u7684CSV\u683c\u5f0f\u6587\u4ef6\u4f1a\u88ab\u63a5\u53d7\u3002", "Only the parent course staff of a CCX can create content groups.": "\u53ea\u6709CCX\u8bfe\u7a0b\u7684\u4e3b\u6559\u5458\u624d\u53ef\u521b\u5efa\u5185\u5bb9\u7ec4\u3002", + "Open": "\u6253\u5f00", "Open Calculator": "\u5f00\u542f\u8ba1\u7b97\u5668", "Open language menu": "\u6253\u5f00\u8bed\u8a00\u529f\u80fd\u83dc\u5355", "Open the certificate you earned for the %(title)s program.": "\u6253\u5f00\u60a8\u5df2\u83b7\u5f97\u7684%(title)s\u8bfe\u7a0b\u8bc1\u4e66\u3002", @@ -1140,6 +1167,8 @@ "Photo of %(fullName)s's ID": "%(fullName)s\u7684\u8eab\u4efd\u8bc1\u4ef6\u7167\u7247", "Photo requirements:": "\u7167\u7247\u8981\u6c42\uff1a", "Photos don't meet the requirements?": "\u7167\u7247\u4e0d\u7b26\u5408\u8981\u6c42\uff1f", + "Pin": "\u6807\u8bb0", + "Pinned": "\u5df2\u56fa\u5b9a", "Placeholder": "\u5360\u4f4d\u7b26", "Play": "\u64ad\u653e", "Play video": "\u64ad\u653e\u89c6\u9891", @@ -1180,6 +1209,7 @@ "Please wait": "\u8bf7\u7a0d\u5019", "Plugins": "\u63d2\u4ef6", "Post": "\u53d1\u5e03", + "Post type": "\u5e16\u5b50\u7c7b\u578b", "Poster": "\u5c01\u9762", "Practice Exam Completed": "\u5df2\u5b8c\u6210\u6a21\u62df\u8003", "Practice Exam Failed": "\u6a21\u62df\u8003\u5931\u8d25", @@ -1224,6 +1254,7 @@ "Professional Education Verified Certificate": "\u4e13\u4e1a\u6559\u80b2\u8ba4\u8bc1\u8bc1\u4e66", "Profile": "\u4e2a\u4eba\u4e3b\u9875", "Profile Image": "\u8d44\u6599\u7167\u7247", + "Profile Information": "\u7528\u6237\u8d44\u6599\u4fe1\u606f", "Profile Visibility:": "\u8d26\u53f7\u8d44\u6599\u53ef\u89c1\uff1a", "Profile image for {username}": "{username} \u7684\u5934\u50cf", "Program Record": "\u8bfe\u7a0b\u8bb0\u5f55", @@ -1237,6 +1268,8 @@ "Published and Live": "\u5df2\u53d1\u5e03\u5e76\u5728\u7ebf", "Publishing": "\u6b63\u5728\u53d1\u5e03", "Publishing Status": "\u53d1\u5e03\u72b6\u6001", + "Question": "\u95ee\u9898", + "Questions raise issues that need answers. Discussions share ideas and start conversations. (Required)": "\u201c\u95ee\u9898\u201d\u6307\u9700\u8981\u56de\u7b54\u7684\u95ee\u9898\u3002\u201c\u8ba8\u8bba\u201d\u6307\u5206\u4eab\u60f3\u6cd5\u5e76\u5f00\u59cb\u4ea4\u6d41\u3002(\u5fc5\u586b\u9879)", "Queued": "\u5df2\u6392\u961f", "REMAINING COURSES": "\u5269\u4f59\u8bfe\u7a0b", "Re-run Course": "\u91cd\u542f\u8bfe\u7a0b", @@ -1259,6 +1292,7 @@ "Regenerate the user's certificate": "\u91cd\u65b0\u751f\u6210\u7528\u6237\u8bc1\u4e66", "Register with Institution/Campus Credentials": "\u4f7f\u7528\u673a\u6784/\u6821\u56ed\u8d26\u53f7\u6ce8\u518c", "Rejected": "\u62d2\u7edd", + "Related to: %(courseware_title_linked)s": "\u4e0e%(courseware_title_linked)s\u76f8\u5173", "Release Date and Time": "\u516c\u5f00\u65e5\u671f\u53ca\u65f6\u95f4", "Release Date:": "\u516c\u5f00\u65e5\u671f\uff1a", "Release Status:": "\u516c\u5f00\u72b6\u6001\uff1a", @@ -1286,7 +1320,10 @@ "Replace all": "\u5168\u90e8\u66ff\u6362", "Replace with": "\u66ff\u6362\u4e3a", "Reply to Annotation": "\u56de\u590d\u6279\u6ce8", + "Report": "\u62a5\u544a", + "Report abuse": "\u62a5\u544a\u8fb1\u9a82\u95ee\u9898", "Report abuse, topics, and responses": "\u4e3e\u62a5\u6ee5\u7528\u3001\u8bdd\u9898\u548c\u56de\u590d", + "Reported": "\u5df2\u62a5\u544a", "Requester": "\u8bf7\u6c42\u8005", "Required": "\u5fc5\u586b", "Required field.": "\u5fc5\u586b\u5b57\u6bb5\u3002", @@ -1305,6 +1342,7 @@ "Return and add email address": "\u8fd4\u56de\u5e76\u6dfb\u52a0\u90ae\u7bb1", "Return to Export": "\u8fd4\u56de\u81f3\u5bfc\u51fa\u9875\u9762", "Return to Your Dashboard": "\u8fd4\u56de\u60a8\u7684\u63a7\u5236\u9762\u677f", + "Return to all posts": "\u56de\u590d\u6240\u6709\u7684\u8ba8\u8bba\u5e16", "Return to team listing": "\u8fd4\u56de\u56e2\u961f\u5217\u8868", "Review Policy Exception": "\u5ba1\u6838\u653f\u7b56\u7684\u7279\u6b8a\u60c5\u51b5", "Review Rules": "\u5ba1\u67e5\u89c4\u5219", @@ -1680,6 +1718,10 @@ "This post could not be reopened. Refresh the page and try again.": "\u65e0\u6cd5\u91cd\u5f00\u6b64\u5e16\u5b50\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u5e76\u91cd\u8bd5\u3002", "This post could not be unflagged for abuse. Refresh the page and try again.": "\u65e0\u6cd5\u53d6\u6d88\u6b64\u5e16\u5b50\u7684\u6ee5\u7528\u4e3e\u62a5\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u5e76\u91cd\u8bd5\u3002", "This post could not be unpinned. Refresh the page and try again.": "\u65e0\u6cd5\u53d6\u6d88\u7f6e\u9876\u6b64\u5e16\u5b50\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u5e76\u91cd\u8bd5\u3002", + "This post is visible only to %(group_name)s.": "\u6b64\u5e16\u53ea\u5bf9%(group_name)s\u7ec4\u53ef\u89c1\u3002", + "This post is visible to everyone.": "\u6b64\u5e16\u5bf9\u6240\u6709\u4eba\u53ef\u89c1\u3002", + "This post will be visible only to %(group_name)s.": "\u6b64\u5e16\u5b50\u4ec5%(group_name)s\u53ef\u89c1\u3002", + "This post will be visible to everyone.": "\u6b64\u5e16\u5b50\u5bf9\u6240\u6709\u4eba\u53ef\u89c1\u3002", "This problem could not be saved.": "\u8be5\u95ee\u9898\u65e0\u6cd5\u4fdd\u5b58\u3002", "This problem has been reset.": "\u6b64\u95ee\u9898\u5df2\u91cd\u7f6e\u3002", "This response could not be marked as an answer. Refresh the page and try again.": "\u65e0\u6cd5\u5c06\u8be5\u56de\u590d\u6807\u8bb0\u4e3a\u7b54\u6848\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u5e76\u91cd\u8bd5\u3002", @@ -1692,6 +1734,7 @@ "This short name for the assignment type (for example, HW or Midterm) appears next to assignments on a learner's Progress page.": "\u4efb\u52a1\u7c7b\u578b\u7b80\u79f0\uff08\u6bd4\u5982\uff1aHW \u6216 Midterm\uff09\uff0c\u663e\u793a\u4e8e\u5b66\u5458\u5b66\u4e60\u8fdb\u5ea6\u9875\u9762\u7684\u4f5c\u4e1a\u65c1\u8fb9\u3002", "This team does not have any members.": "\u672c\u56e2\u961f\u6ca1\u6709\u6210\u5458\u3002", "This team is full.": "\u8fd9\u4e2a\u56e2\u961f\u5df2\u7ecf\u6ee1\u4e86\u3002", + "This thread is closed.": "\u8fd9\u4e2a\u5e16\u5b50\u5df2\u7ecf\u5173\u95ed\u3002", "This unit has validation issues.": "\u6b64\u5355\u5143\u5b58\u5728\u9a8c\u8bc1\u95ee\u9898\u3002", "This vote could not be processed. Refresh the page and try again.": "\u65e0\u6cd5\u5904\u7406\u6b64\u6295\u7968\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u5e76\u91cd\u8bd5\u3002", "This {parentCategory} has no {childCategory}": "\u6b64 {parentCategory} \u4e0d\u5305\u542b {childCategory}", @@ -1730,6 +1773,7 @@ "Tools": "\u5de5\u5177", "Top": "\u9876\u7aef", "Topic": "\u4e3b\u9898", + "Topic area": "\u4e3b\u9898\u8303\u56f4", "Topics": "\u4e3b\u9898", "Total": "\u603b\u8ba1", "Total Number": "\u603b\u6570", @@ -1759,7 +1803,9 @@ "Undo Changes": "\u64a4\u9500\u66f4\u6539", "Undo move": "\u64a4\u9500\u79fb\u52a8", "Undo moving": "\u64a4\u9500\u79fb\u52a8", + "Unendorse": "\u53d6\u6d88\u652f\u6301", "Unexpected server error.": "\u670d\u52a1\u5668\u5f02\u5e38\u9519\u8bef\u3002", + "Unfollow": "\u53d6\u6d88\u5173\u6ce8", "Ungraded": "\u672a\u5206\u7ea7", "Ungraded Practice Exam": "\u4e0d\u8ba1\u5206\u6a21\u62df\u8003\u8bd5", "Unit": "\u5355\u5143", @@ -1772,14 +1818,20 @@ "Unlink This Account": "\u89e3\u7ed1\u6b64\u8d26\u53f7", "Unlink your {accountName} account": "\u89e3\u7ed1\u60a8\u7684{accountName}\u8d26\u53f7", "Unlinking": "\u89e3\u7ed1\u4e2d", + "Unmark as Answer": "\u53d6\u6d88\u6807\u8bb0\u4f5c\u4e3a\u7b54\u6848", "Unmute": "\u53d6\u6d88\u9759\u97f3", "Unnamed Option": "\u672a\u547d\u540d\u9009\u9879", + "Unpin": "\u4e0d\u505a\u6807\u8bb0", "Unpublished changes to content that will release in the future": "\u5c06\u5728\u672a\u6765\u516c\u5f00\u7684\u3001\u5c1a\u672a\u53d1\u5e03\u7684\u5185\u5bb9\u66f4\u65b0", "Unpublished changes to live content": "\u5bf9\u5728\u7ebf\u5185\u5bb9\u6709\u672a\u53d1\u5e03\u7684\u66f4\u6539", "Unpublished units will not be released": "\u5c1a\u672a\u53d1\u5e03\u7684\u5355\u5143\u5c06\u4e0d\u4f1a\u88ab\u516c\u5f00", + "Unreport": "\u53d6\u6d88\u62a5\u544a", "Unscheduled": "\u5c1a\u672a\u8ba1\u5212", "Update": "\u66f4\u65b0", "Update Settings": "\u66f4\u65b0\u8bbe\u7f6e", + "Update comment": "\u66f4\u65b0\u8bc4\u8bba", + "Update post": "\u66f4\u65b0\u8ba8\u8bba\u5e16", + "Update response": "\u66f4\u65b0\u56de\u590d", "Update team.": "\u66f4\u65b0\u56e2\u961f\u3002", "Updating Tags": "\u66f4\u65b0\u6807\u7b7e\u4e2d", "Updating with latest library content": "\u66f4\u65b0\u6700\u65b0\u7684\u5e93\u5185\u5bb9", @@ -1829,6 +1881,7 @@ "Use cohorts as the basis for dividing discussions. All learners, regardless of cohort, see the same discussion topics, but within divided topics, only members of the same cohort see and respond to each others\u2019 posts. ": "\u6309\u7fa4\u7ec4\u533a\u5206\u8ba8\u8bba\u7ec4\uff0c\u6240\u6709\u5b66\u5458\uff0c\u4e0d\u7ba1\u6240\u5728\u4ec0\u4e48\u7fa4\u7ec4\uff0c\u90fd\u53ef\u4ee5\u770b\u5230\u540c\u6837\u7684\u8ba8\u8bba\u4e3b\u9898\uff1b\u4f46\u5728\u4e0d\u540c\u7684\u4e3b\u9898\u4e2d\uff0c\u53ea\u6709\u540c\u4e00\u7fa4\u7ec4\u7684\u6210\u5458\u53ef\u4ee5\u67e5\u770b\u548c\u56de\u590d\u5f7c\u6b64\u7684\u5e16\u5b50\u3002", "Use enrollment tracks as the basis for dividing discussions. All learners, regardless of their enrollment track, see the same discussion topics, but within divided topics, only learners who are in the same enrollment track see and respond to each others\u2019 posts.": "\u5c06\u9009\u8bfe\u901a\u9053\u4f5c\u4e3a\u5212\u5206\u8ba8\u8bba\u7ec4\u7684\u57fa\u7840\u3002\u6240\u6709\u7684\u5b66\u5458\uff0c\u4e0d\u7ba1\u4ed6\u4eec\u7684\u9009\u8bfe\u901a\u9053\u662f\u4ec0\u4e48\uff0c\u90fd\u53ef\u4ee5\u770b\u5230\u76f8\u540c\u7684\u8ba8\u8bba\u4e3b\u9898\uff0c\u4f46\u662f\u5728\u4e0d\u540c\u7684\u4e3b\u9898\u4e2d\uff0c\u53ea\u6709\u5728\u540c\u4e00\u9009\u8bfe\u901a\u9053\u4e0a\u7684\u5b66\u5458\u624d\u80fd\u770b\u5230\u548c\u56de\u590d\u5f7c\u6b64\u7684\u5e16\u5b50\u3002", "Use my institution/campus credentials": "\u4f7f\u7528\u6211\u7684\u673a\u6784/\u6821\u56ed\u8d26\u53f7", + "Use my university info": "\u4f7f\u7528\u6211\u7684\u5927\u5b66\u4fe1\u606f", "Use the All Topics menu to find specific topics.": "\u4f7f\u7528\u201c\u6240\u6709\u4e3b\u9898\u201d\u83dc\u5355\u627e\u5230\u7279\u5b9a\u8bdd\u9898", "Use your webcam to take a photo of your face. We will match this photo with the photo on your ID.": "\u8bf7\u7528\u6444\u50cf\u5934\u62cd\u6444\u4e00\u5f20\u60a8\u7684\u9762\u90e8\u7167\u7247\uff0c\u6211\u4eec\u5c06\u5bf9\u6bd4\u8be5\u7167\u7247\u4e0e\u60a8\u8eab\u4efd\u8bc1\u4ef6\u4e0a\u7684\u7167\u7247\u3002", "Used": "\u5df2\u4f7f\u7528", @@ -1872,21 +1925,25 @@ "View Archived Course": "\u67e5\u770b\u5b58\u6863\u7684\u8bfe\u7a0b", "View Cohort": "\u67e5\u770b\u7fa4\u7ec4", "View Course": "\u67e5\u770b\u8bfe\u7a0b", + "View Current Team Memberships": "\u67e5\u770b\u5f53\u524d\u56e2\u961f\u6210\u5458", "View Live": "\u5728\u7ebf\u67e5\u770b", "View Program Record": "\u67e5\u770b\u8bfe\u7a0b\u8bb0\u5f55", "View Teams in the {topic_name} Topic": "\u67e5\u770b {topic_name} \u4e3b\u9898\u4e0b\u7684\u56e2\u961f", "View all errors": "\u67e5\u770b\u6240\u6709\u9519\u8bef", "View child items": "\u67e5\u770b\u5b50\u7c7b\u76ee", + "View discussion": "\u67e5\u770b\u8ba8\u8bba", "View my exam": "\u67e5\u770b\u6211\u7684\u8003\u8bd5", "View {span_start} {team_name} {span_end}": "\u67e5\u770b {span_start} {team_name} {span_end}", "Viewing %s course": [ "\u67e5\u770b %s \u4e2a\u8bfe\u7a0b" ], "Visibility": "\u53ef\u89c1\u6027", + "Visible to": "\u5bf9\u5176\u53ef\u89c1", "Visible to Staff Only": "\u4ec5\u5de5\u4f5c\u4eba\u5458\u53ef\u89c1", "Visual aids": "\u7f51\u683c\u7ebf", "Volume": "\u97f3\u91cf", "Vote for good posts and responses": "\u4e3a\u51fa\u8272\u7684\u53d1\u5e16\u548c\u56de\u590d\u6295\u7968", + "Vote for this post,": "\u4e3a\u8be5\u5e16\u6295\u7968", "Waiting": "\u7b49\u5f85", "Want to make edX better for everyone?": "\u60f3\u8981\u8ba9edX \u4e3a\u6bcf\u4e2a\u4eba\u53d8\u5f97\u66f4\u597d?", "Warning": "\u8b66\u544a", @@ -1899,6 +1956,7 @@ "We have encountered an error. Refresh your browser and then try again.": "\u53d1\u751f\u9519\u8bef\uff0c\u8bf7\u5237\u65b0\u60a8\u7684\u6d4f\u89c8\u5668\u5e76\u91cd\u8bd5\u3002", "We just need a little more information before you start learning with %(platformName)s.": "\u60a8\u53ea\u9700\u518d\u591a\u63d0\u4f9b\u4e00\u70b9\u4fe1\u606f\u5c31\u53ef\u4ee5\u5f00\u59cb\u5728%(platformName)s\u5b66\u4e60\u4e86\u3002", "We use the highest levels of security available to encrypt your photo and send it to our authorization service for review. Your photo and information are not saved or visible anywhere on %(platformName)s after the verification process is complete.": "\u6211\u4eec\u4f1a\u91c7\u7528\u6700\u9ad8\u7ea7\u522b\u7684\u5b89\u5168\u6280\u672f\u6765\u52a0\u5bc6\u60a8\u7684\u7167\u7247\u5e76\u53d1\u9001\u5230\u6211\u4eec\u7684\u6388\u6743\u670d\u52a1\u7528\u4e8e\u5ba1\u6838\u76ee\u7684\uff1b\u4e00\u65e6\u5b8c\u6210\u4e86\u8ba4\u8bc1\u8fc7\u7a0b\uff0c%(platformName)s\u4e0d\u4f1a\u7ee7\u7eed\u4fdd\u5b58\u8fd9\u4e9b\u7167\u7247\u548c\u4fe1\u606f\u3002", + "We use your verification photos to confirm your identity and ensure the validity of your certificate.": "\u6211\u4eec\u4f7f\u7528\u60a8\u7684\u9a8c\u8bc1\u7167\u7247\u6765\u786e\u8ba4\u60a8\u7684\u8eab\u4efd\u5e76\u786e\u4fdd\u60a8\u7684\u8bc1\u4e66\u7684\u6709\u6548\u6027\u3002", "We're sorry to see you go! Your account will be deleted shortly.": "\u5f88\u9057\u61be\u60a8\u8981\u79bb\u5f00\uff01\u60a8\u7684\u8d26\u53f7\u5c06\u5f88\u5feb\u88ab\u5220\u9664\u3002", "We're sorry, there was an error": "\u5f88\u62b1\u6b49\uff0c\u6b64\u5904\u51fa\u73b0\u9519\u8bef", "We've encountered an error. Refresh your browser and then try again.": "\u6211\u4eec\u9047\u5230\u4e86\u4e00\u4e2a\u9519\u8bef\u3002\u8bf7\u5237\u65b0\u60a8\u7684\u6d4f\u89c8\u5668\u5e76\u91cd\u8bd5\u3002", @@ -1913,6 +1971,8 @@ "What can we help you with, {username}?": "\u4eb2\u7231\u7684{username}\uff0c\u6709\u4ec0\u4e48\u53ef\u4ee5\u5e2e\u52a9\u60a8\uff1f", "What does %(platformName)s do with this photo?": "%(platformName)s\u7528\u8fd9\u5f20\u7167\u7247\u505a\u4ec0\u4e48\uff1f", "What does this mean?": "\u8fd9\u662f\u4ec0\u4e48\u610f\u601d\uff1f", + "What if I have difficulty holding my ID in position relative to the camera?": "\u5982\u679c\u6211\u96be\u4ee5\u5c06\u6211\u7684 ID \u4fdd\u6301\u5728\u76f8\u5bf9\u4e8e\u76f8\u673a\u7684\u4f4d\u7f6e\u600e\u4e48\u529e\uff1f", + "What if I have difficulty holding my head in position relative to the camera?": "\u5982\u679c\u6211\u76f8\u5bf9\u76f8\u673a\u65e0\u6cd5\u4fdd\u6301\u5934\u90e8\u59ff\u52bf\u600e\u4e48\u529e\uff1f", "What was the total combined income, during the last 12 months, of all members of your family? ": "\u5728\u8fc7\u53bb\u768412\u4e2a\u6708\u4e2d\uff0c\u60a8\u5bb6\u5ead\u6240\u6709\u4eba\u7684\u603b\u6536\u5165\u662f\u591a\u5c11\uff1f", "When learners submit an answer to an assessment, they immediately see whether the answer is correct or incorrect, and the score received.": "\u5f53\u5b66\u5458\u63d0\u4ea4\u4e00\u4efd\u7b54\u6848\u81f3\u8bc4\u4f30\u65f6\uff0c\u4ed6\u4eec\u53ef\u4ee5\u7acb\u5373\u67e5\u770b\u7b54\u6848\u662f\u5426\u6b63\u786e\u548c\u6240\u5f97\u5206\u6570\u3002", "Which timed transcript would you like to use?": "\u60a8\u60f3\u4f7f\u7528\u54ea\u4e2a\u5b57\u5e55\uff1f", @@ -1939,6 +1999,7 @@ "You are not enrolled in any programs yet.": "\u60a8\u5c1a\u672a\u52a0\u5165\u4efb\u4f55\u8bfe\u7a0b\u3002", "You are now enrolled as a verified student for:": "\u60a8\u5df2\u7ecf\u5df2\u8ba4\u8bc1\u5b66\u751f\u7684\u8eab\u4efd\u9009\u62e9\u4e86\u8bfe\u7a0b\uff1a", "You are sending an email message with the subject {subject} to the following recipients.": "\u60a8\u6b63\u5728\u53d1\u9001\u542b\u6709{subject}\u4e3b\u9898\u7684\u7535\u5b50\u90ae\u4ef6\u7ed9\u4ee5\u4e0b\u6536\u4ef6\u4eba\u3002", + "You are taking \"{exam_link}\" as {exam_type}. ": "\u60a8\u5c06\u201c{exam_link}\u201d\u8bbe\u4e3a{exam_type}\u3002", "You are upgrading your enrollment for: {courseName}": "\u60a8\u6b63\u5728\u5347\u7ea7\u60a8\u7684 {courseName} \u9009\u8bfe\u72b6\u6001", "You can change sessions until {expiration_date}.": "\u60a8\u53ef\u5728{expiration_date}\u524d\u66f4\u6539\u5b66\u671f\u3002", "You can link your social media accounts to simplify signing in to {platform_name}.": "\u60a8\u53ef\u4ee5\u5173\u8054\u60a8\u7684\u793e\u4ea4\u5a92\u4f53\u8d26\u53f7\u6765\u767b\u5f55{platform_name}\u3002", @@ -2058,6 +2119,7 @@ "and others": "\u5176\u4ed6", "anonymous": "\u533f\u540d", "answer": "\u7b54\u6848", + "answered question": "\u5df2\u56de\u590d\u7684\u95ee\u9898", "asset_path is required": "asset_path\u5fc5\u586b", "bytes": "\u5b57\u8282", "certificate": "\u8bc1\u4e66", @@ -2071,6 +2133,8 @@ "delete chapter": "\u5220\u9664\u7ae0", "delete group": "\u5220\u9664\u7ec4", "details about the failure": "\u5931\u8d25\u8be6\u7ec6", + "discussion": "\u8ba8\u8bba", + "discussion posted %(time_ago)s by %(author)s": "\u7531%(author)s\u4e8e%(time_ago)s\u53d1\u5e03\u6b64\u8ba8\u8bba\u5e16", "dragging": "\u62d6\u62fd", "dragging out of slider": "\u62d6\u62fd\u51fa\u6ed1\u5757\u533a\u57df", "dropped in slider": "\u5728\u6ed1\u5757\u4e2d\u653e\u4e0b", @@ -2080,8 +2144,11 @@ "e.g. 'http://google.com'": "\u4f8b\u5982\u201chttp://google.com/\u201d", "e.g. johndoe@example.com, JaneDoe, joeydoe@example.com": "\u4f8b\u5982\uff1ajohndoe@example.com, JaneDoe, joeydoe@example.com", "emphasized text": "\u5f3a\u8c03\u6587\u5b57", + "endorsed %(time_ago)s": "%(time_ago)s\u524d\u83b7\u5f97\u652f\u6301", + "endorsed %(time_ago)s by %(user)s": "%(time_ago)s\u524d\u83b7\u5f97%(user)s\u7684\u652f\u6301", "enter code here": "\u6b64\u5904\u8f93\u5165\u4ee3\u7801", "enter link description here": "\u6b64\u5904\u8f93\u5165\u94fe\u63a5\u7684\u63cf\u8ff0", + "follow this post": "\u5173\u6ce8\u8fd9\u4e2a\u5e16\u5b50", "for": "\u7684", "group configuration": "\u7ec4\u914d\u7f6e", "image omitted": "\u7701\u7565\u7684\u56fe\u7247", @@ -2089,6 +2156,8 @@ "internally reviewed": "\u5185\u90e8\u5ba1\u6838", "last activity": "\u6700\u540e\u6d3b\u52a8", "less than a minute": "\u5c11\u4e8e\u4e00\u5206\u949f", + "marked as answer %(time_ago)s": "%(time_ago)s\u524d\u88ab\u6807\u8bb0\u4e3a\u7b54\u6848 ", + "marked as answer %(time_ago)s by %(user)s": "%(time_ago)s \u524d\u88ab%(user)s\u6807\u8bb0\u4e3a\u7b54\u6848", "minute": "\u5206", "minutes": "\u5206", "name": "\u540d\u79f0", @@ -2100,9 +2169,12 @@ "or create a new one here": "\u6216\u5728\u6b64\u521b\u5efa\u4e00\u4e2a\u65b0\u8d26\u53f7", "or sign in with": "\u6216\u8005\u901a\u8fc7\u4ee5\u4e0b\u65b9\u5f0f\u767b\u5f55", "path/to/introductionToCookieBaking-CH{order}.pdf": "path/to/introductionToCookieBaking-CH{order}.pdf", + "post anonymously to classmates": "\u5411\u540c\u5b66\u533f\u540d\u53d1\u5e16", + "posted %(time_ago)s by %(author)s": "%(author)s\u5728%(time_ago)s\u524d\u53d1\u8868", "price": "\u4ef7\u683c", "provide the title/name of the chapter that will be used in navigating": "\u63d0\u4f9b\u5c06\u7528\u5728\u5bfc\u822a\u4e2d\u7684\u7ae0\u6807\u9898\uff0f\u540d\u79f0", "provide the title/name of the text book as you would like your students to see it": "\u63d0\u4f9b\u60a8\u5e0c\u671b\u5b66\u751f\u770b\u5230\u7684\u8bfe\u672c\u6807\u9898\uff0f\u540d\u79f0", + "question posted %(time_ago)s by %(author)s": "\u7531%(author)s\u4e8e%(time_ago)s\u53d1\u5e03\u6b64\u95ee\u9898", "remove": "\u79fb\u9664", "remove all": "\u5168\u90e8\u79fb\u9664", "second": "\u79d2", @@ -2121,6 +2193,7 @@ "title_word_{uniqueId}": "title_word_{uniqueId}", "toggle chapter %(displayName)s": "\u5207\u6362%(displayName)s\u7ae0", "toggle subsection %(displayName)s": "\u5207\u6362%(displayName)s\u5c0f\u8282", + "unanswered question": "\u5f85\u56de\u590d\u7684\u95ee\u9898", "unit": "\u5355\u5143", "unsubmitted": "\u672a\u63d0\u4ea4", "upload a PDF file or provide the path to a Studio asset file": "\u4e0a\u4f20 PDF \u6587\u4ef6\u6216\u63d0\u4f9b\u6307\u5411 Studio \u8d44\u6e90\u6587\u4ef6\u7684\u8def\u5f84", @@ -2194,6 +2267,7 @@ "{totalItems} total": "\u5171{totalItems} ", "{transcriptClientTitle}_{transcriptLanguageCode}.{fileExtension}": "{transcriptClientTitle}_{transcriptLanguageCode}.{fileExtension}", "{type} Progress": "{type}\u8fdb\u5ea6", + "{unread_comments_count} new": "{unread_comments_count} \u65b0\u8bc4\u8bba", "\u2026": "\u2026" }; for (const key in newcatalog) { diff --git a/cms/static/js/i18n/zh-tw/djangojs.js b/cms/static/js/i18n/zh-tw/djangojs.js index 52869bff7917..bbd8d107b3d3 100644 --- a/cms/static/js/i18n/zh-tw/djangojs.js +++ b/cms/static/js/i18n/zh-tw/djangojs.js @@ -39,7 +39,6 @@ "A short description of the team to help other learners understand the goals or direction of the team (maximum 300 characters).": "\u7c21\u77ed\u7684\u5718\u968a\u63cf\u8ff0\uff0c\u4ee5\u52a9\u65bc\u5176\u4ed6\u5b78\u7fd2\u8005\u4e86\u89e3\u6b64\u5718\u968a\u7684\u76ee\u6a19\u8207\u65b9\u5411 (\u6700\u5927\u70ba255\u500b\u5b57\u5143\u9577\u5ea6)", "A valid email address is required": "\u9700\u8981\u4e00\u500b\u6709\u6548\u7684\u96fb\u5b50\u90f5\u4ef6\u5730\u5740", "ABCDEFGHIJKLMNOPQRSTUVWXYZ": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "About You": "\u95dc\u65bc\u60a8", "Account Information": "\u5e33\u865f\u8cc7\u8a0a", "Account Not Activated": "\u5e33\u865f\u672a\u555f\u7528", "Account Settings": "\u5e33\u865f\u8a2d\u5b9a", @@ -98,7 +97,6 @@ "Assign students to cohorts by uploading a CSV file.": "\u4e0a\u50b3\u4e00\u500b CSV\u6a94\u6848\u4f86\u5206\u914d\u5b78\u751f\u5230\u5b78\u7fd2\u5925\u4f34\u7fa4\u7d44\u3002", "Automatic": "\u81ea\u52d5", "Back to sign in": "\u8fd4\u56de\u767b\u5165\u9801\u9762", - "Back to {platform} FAQs": "\u56de\u5230 {platform} FAQs", "Basic": "\u57fa\u672c", "Basic Account Information": "\u57fa\u672c\u5e33\u865f\u8cc7\u8a0a", "Be sure your entire face is inside the frame": "\u8acb\u78ba\u8a8d\u60a8\u7684\u6574\u500b\u81c9\u90e8\u90fd\u5728\u65b9\u6846\u5167", diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js index cde0b42d8109..28ea2a4b9196 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -581,33 +581,6 @@ describe('Container Subviews', function() { }); }); - describe('PublishHistory', function() { - var lastPublishCss = '.wrapper-last-publish'; - - it('renders never published when the block is unpublished', function() { - renderContainerPage(this, mockContainerXBlockHtml, { - published: false, published_on: null, published_by: null - }); - expect(containerPage.$(lastPublishCss).text()).toContain('Never published'); - }); - - it('renders the last published date and user when the block is published', function() { - renderContainerPage(this, mockContainerXBlockHtml); - fetch({ - published: true, published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako' - }); - expect(containerPage.$(lastPublishCss).text()) - .toContain('Last published Jul 01, 2014 at 12:45 UTC by amako'); - }); - - it('renders correctly when the block is published without publish info', function() { - renderContainerPage(this, mockContainerXBlockHtml); - fetch({ - published: true, published_on: null, published_by: null - }); - expect(containerPage.$(lastPublishCss).text()).toContain('Previously published'); - }); - }); describe('Message Area', function() { var messageSelector = '.container-message .warning', diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index e624021b47ef..783133af21be 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -32,7 +32,7 @@ function($, _, Backbone, gettext, BasePage, 'click .new-component-button': 'scrollToNewComponentButtons', 'click .save-button': 'saveSelectedLibraryComponents', 'click .paste-component-button': 'pasteComponent', - 'click .tags-button': 'openManageTags', + 'click .manage-tags-button': 'openManageTags', 'change .header-library-checkbox': 'toggleLibraryComponent', 'click .collapse-button': 'collapseXBlock', }, @@ -77,6 +77,7 @@ function($, _, Backbone, gettext, BasePage, model: this.model }); this.messageView.render(); + this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel"); // Display access message on units and split test components if (!this.isLibraryPage) { this.containerAccessView = new ContainerSubviews.ContainerAccess({ @@ -89,7 +90,8 @@ function($, _, Backbone, gettext, BasePage, el: this.$('#publish-unit'), model: this.model, // When "Discard Changes" is clicked, the whole page must be re-rendered. - renderPage: this.render + renderPage: this.render, + clipboardBroadcastChannel: this.clipboardBroadcastChannel, }); this.xblockPublisher.render(); @@ -120,7 +122,6 @@ function($, _, Backbone, gettext, BasePage, } this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved); - this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel"); }, getViewParameters: function() { @@ -175,6 +176,13 @@ function($, _, Backbone, gettext, BasePage, if (!self.isLibraryPage && !self.isLibraryContentPage) { self.initializePasteButton(); } + + var targetId = window.location.hash.slice(1); + if (targetId) { + var target = document.getElementById(targetId); + target.scrollIntoView({ behavior: 'smooth', inline: 'center' }); + } + }, block_added: options && options.block_added }); diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index b4ee286ae897..8848abc246e2 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -107,7 +107,8 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H events: { 'click .action-publish': 'publish', 'click .action-discard': 'discardChanges', - 'click .action-staff-lock': 'toggleStaffLock' + 'click .action-staff-lock': 'toggleStaffLock', + 'click .action-copy': 'copyToClipboard' }, // takes XBlockInfo as a model @@ -117,6 +118,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H this.template = this.loadTemplate('publish-xblock'); this.model.on('sync', this.onSync, this); this.renderPage = this.options.renderPage; + this.clipboardBroadcastChannel = this.options.clipboardBroadcastChannel; }, onSync: function(model) { @@ -148,6 +150,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H releaseDateFrom: this.model.get('release_date_from'), hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'), staffLockFrom: this.model.get('staff_lock_from'), + enableCopyUnit: this.model.get('enable_copy_paste_units'), course: window.course, HtmlUtils: HtmlUtils }) @@ -174,6 +177,50 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H }); }, + copyToClipboard: function(e) { + e.preventDefault(); + e.stopPropagation(); + const clipboardEndpoint = "/api/content-staging/v1/clipboard/"; + const usageKeyToCopy = this.model.get('id'); + // Start showing a "Copying" notification: + ViewUtils.runOperationShowingMessage(gettext('Copying'), () => { + return $.postJSON( + clipboardEndpoint, + { usage_key: usageKeyToCopy }, + ).then((data) => { + const status = data.content?.status; + if (status === "ready") { + // something that enables the paste button in the actions dropdown + this.clipboardBroadcastChannel.postMessage(data); + return data; + } else if (status === "loading") { + // The clipboard is being loaded asynchonously. + // Poll the endpoint until the copying process is complete: + const deferred = $.Deferred(); + const checkStatus = () => { + $.getJSON(clipboardEndpoint, (pollData) => { + const newStatus = pollData.content?.status; + if (newStatus === "ready") { + // something that enables the paste button in actions dropdown + this.clipboardBroadcastChannel.postMessage(pollData); + deferred.resolve(pollData); + } else if (newStatus === "loading") { + setTimeout(checkStatus, 1_000); + } else { + deferred.reject(); + throw new Error(`Unexpected clipboard status "${newStatus}" in successful API response.`); + } + }) + } + setTimeout(checkStatus, 1_000); + return deferred; + } else { + throw new Error(`Unexpected clipboard status "${status}" in successful API response.`); + } + }); + }); + }, + discardChanges: function(e) { var xblockInfo = this.model, renderPage = this.renderPage; @@ -418,6 +465,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H tagContentElement.ariaExpanded = "false"; tagContentElement.setAttribute('aria-controls', `content-tags-tag-${tag.id}`); tagContentElement.appendChild(tagIconElement); + tagContentElement.className += ' tagging-label-link'; parentElement.appendChild(tagChildrenElement); // Render children diff --git a/cms/static/js/views/utils/xblock_utils.js b/cms/static/js/views/utils/xblock_utils.js index d3c1fce9e00e..9abe0866ed48 100644 --- a/cms/static/js/views/utils/xblock_utils.js +++ b/cms/static/js/views/utils/xblock_utils.js @@ -8,7 +8,7 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) { var addXBlock, duplicateXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState, getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields, getXBlockType, findXBlockInfo, - moveXBlock; + moveXBlock, pasteXBlock; /** * Represents the possible visibility states for an xblock: @@ -69,6 +69,85 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) { }); }; + pasteXBlock = function(target) { + var parentLocator = target.data('parent'), + displayName = target.data('default-name'); + + return ViewUtils.runOperationShowingMessage(gettext('Pasting'), () => { + return $.postJSON(ModuleUtils.getUpdateUrl(), { + parent_locator: parentLocator, + staged_content: "clipboard", + }).then((data) => { + return data; + }); + }).done((data) => { + const { + conflicting_files: conflictingFiles, + error_files: errorFiles, + new_files: newFiles, + } = data.static_file_notices; + + const notices = []; + if (errorFiles.length) { + notices.push((next) => new PromptView.Error({ + title: gettext("Some errors occurred"), + message: ( + gettext("The following required files could not be added to the course:") + + " " + errorFiles.join(", ") + ), + actions: {primary: {text: gettext("OK"), click: (x) => { x.hide(); next(); }}}, + })); + } + if (conflictingFiles.length) { + notices.push((next) => new PromptView.Warning({ + title: gettext("You may need to update a file(s) manually"), + message: ( + gettext( + "The following files already exist in this course but don't match the " + + "version used by the component you pasted:" + ) + " " + conflictingFiles.join(", ") + ), + actions: {primary: {text: gettext("OK"), click: (x) => { x.hide(); next(); }}}, + })); + } + if (newFiles.length) { + notices.push(() => new NotificationView.Info({ + title: gettext("New file(s) added to Files & Uploads."), + message: ( + gettext("The following required files were imported to this course:") + + " " + newFiles.join(", ") + ), + actions: { + primary: { + text: gettext('View files'), + click: function(notification) { + const article = document.querySelector('[data-course-assets]'); + const assetsUrl = $(article).attr('data-course-assets'); + window.location.href = assetsUrl; + return; + } + }, + secondary: { + text: gettext('Dismiss'), + click: function(notification) { + return notification.hide(); + } + } + } + })); + } + if (notices.length) { + // Show the notices, one at a time: + const showNext = () => { + const view = notices.shift()(showNext); + view.show(); + } + // Delay to avoid conflict with the "Pasting..." notification. + setTimeout(showNext, 1250); + } + }); + }; + /** * Duplicates the specified xblock element in its parent xblock. * @param {jquery Element} xblockElement The xblock element to be duplicated. @@ -308,6 +387,7 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) { getXBlockListTypeClass: getXBlockListTypeClass, updateXBlockFields: updateXBlockFields, getXBlockType: getXBlockType, - findXBlockInfo: findXBlockInfo + findXBlockInfo: findXBlockInfo, + pasteXBlock: pasteXBlock }; }); diff --git a/cms/static/js/views/xblock.js b/cms/static/js/views/xblock.js index 6b913d5239da..adedd2e2c093 100644 --- a/cms/static/js/views/xblock.js +++ b/cms/static/js/views/xblock.js @@ -14,6 +14,10 @@ function($, _, ViewUtils, BaseView, XBlock, HtmlUtils) { 'click .notification-action-button': 'fireNotificationActionEvent' }, + options: { + clipboardData: { content: null }, + }, + initialize: function() { BaseView.prototype.initialize.call(this); this.view = this.options.view; diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss index 97ff8b8aacfb..453108c0e548 100644 --- a/cms/static/sass/elements/_navigation.scss +++ b/cms/static/sass/elements/_navigation.scss @@ -288,6 +288,11 @@ $seq-nav-height: 40px; ol { display: flex; + .custom-dropdown { + position: relative; + display: inline-flex; + } + li { box-sizing: border-box; min-width: 40px; @@ -300,6 +305,47 @@ $seq-nav-height: 40px; @include border-right-style(solid); } + .dropdown-main-button { + border-right: 1px solid #e7e7e7 !important; + } + + .dropdown-toggle-button { + width: 15% !important; + + &:hover { + border-bottom: 1px solid #e7e7e7 !important; + } + } + + .dropdown-options { + position: absolute; + top: 100%; + z-index: 1000; + background-color: #ffffff; + min-width: 265px; + right: 0; + + li { + padding: 0.5em 1em; + cursor: pointer; + + a { + display: block; + width: 100%; + color: black; + } + + .checkmark { + float: right; + margin-left: 10px; + } + } + } + + .dropdown-options li:hover { + background-color: #f1f1f1; + } + button { @extend %ui-fake-link; @extend %ui-clear-button; diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index 2b55b00cb6f4..f28ac839c569 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -239,6 +239,19 @@ color: $gray-l1; } } + + .action-copy { + width: 100%; + border-color: #0075b4; + padding-top: 10px; + padding-bottom: 10px; + line-height: 24px; + border-radius: 4px; + + &:hover { + @extend %btn-primary-blue; + } + } } } @@ -251,6 +264,7 @@ .wrapper-tag-header { display: flex; justify-content: space-between; + border: 1px dotted transparent; .tag-title { font-weight: bold; @@ -264,8 +278,8 @@ } } - .wrapper-tag-header:focus { - border: 1px dotted gray; + .wrapper-tag-header:focus-visible { + border-color: $gray; } .action-primary { @@ -300,11 +314,18 @@ } } - .tagging-label:hover, - .tagging-label:focus { + .tagging-label-link { + border: 1px dotted transparent; + } + + .tagging-label-link:hover { color: $blue; } + .tagging-label-link:focus-visible { + border-color: $gray; + } + .icon { margin-left: 5px; } diff --git a/cms/templates/container.html b/cms/templates/container.html index 41fe2eb53781..cd9530ed5120 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -53,26 +53,57 @@ clipboardData: ${user_clipboard | n, dump_js_escaped_json}, } ); - require(["js/models/xblock_info", "js/views/xblock", "js/views/utils/xblock_utils", "common/js/components/utils/view_utils"], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils) { + + require(["js/models/xblock_info", "js/views/xblock", "js/views/utils/xblock_utils", "common/js/components/utils/view_utils", "gettext"], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) { var model = new XBlockInfo({ id: '${subsection.location|n, decode.utf8}' }); var xblockView = new XBlockView({ model: model, el: $('#sequence-nav'), - view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}' + view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}', + clipboardData: ${user_clipboard | n, dump_js_escaped_json}, }); + xblockView.xblockReady = function() { - $('.seq_new_button').click(function(evt) { - evt.preventDefault(); - XBlockUtils.addXBlock($(evt.target)).done(function(locator) { + + var toggleCaretButton = function(clipboardData) { + if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes("vertical")) { + $('.dropdown-toggle-button').show(); + } else { + $('.dropdown-toggle-button').hide(); + $('.dropdown-options').hide(); + } + }; + this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel"); + this.clipboardBroadcastChannel.onmessage = (event) => { + toggleCaretButton(event.data); + }; + toggleCaretButton(this.options.clipboardData); + + $('#new-unit-button').on('click', function(event) { + event.preventDefault(); + XBlockUtils.addXBlock($(this)).done(function(locator) { ViewUtils.redirect('/container/' + locator + '?action=new'); - return false; }); - return false; }); + $('.custom-dropdown .dropdown-toggle-button').on('click', function(event) { + event.stopPropagation(); // Prevent the event from closing immediately when we open it + $(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility + var isExpanded = $(this).attr('aria-expanded') === 'true'; + $(this).attr('aria-expanded', !isExpanded); + }); + + $('.seq_paste_unit').on('click', function(event) { + event.preventDefault(); + $('.dropdown-options').hide(); + XBlockUtils.pasteXBlock($(this)).done(function(data) { + ViewUtils.redirect('/container/' + data.locator + '?action=new'); + }); + }); }; + xblockView.render(); }); diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index baf802a29112..be5e1477549b 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -201,14 +201,14 @@ if (is_proctored_exam) { <% } %> <% if (xblockInfo.isVertical()) { %> - <% if (typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %> <% } %> + <% } %> <% if (xblockInfo.isDuplicable()) { %> + <% if (enableCopyUnit) { %> +
+
    +
  • + +
  • +
+
+ <% } %> diff --git a/cms/templates/js/tag-list.underscore b/cms/templates/js/tag-list.underscore index a006eb111b5b..bbea8fdaa411 100644 --- a/cms/templates/js/tag-list.underscore +++ b/cms/templates/js/tag-list.underscore @@ -16,7 +16,7 @@ <% for (var i = 0; i < tags.taxonomies.length; i++) { var taxonomy = tags.taxonomies[i]; %> -