diff --git a/Dockerfile b/Dockerfile index 94bea46e56..6948fefcf8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -119,7 +119,7 @@ RUN nodeenv /edx/app/edxapp/nodeenv --node=16.14.0 --prebuilt RUN npm install -g npm@8.5.x COPY package.json package.json COPY package-lock.json package-lock.json -RUN npm set progress=false && npm install +RUN npm set progress=false && npm ci # The builder-development stage is a temporary stage that installs python modules required for development purposes # The built artifacts from this stage are then copied to the development stage. diff --git a/Makefile b/Makefile index 8a0a6840f4..e075f092f1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Do things in edx-platform -.PHONY: api-docs-sphinx api-docs base-requirements check-types clean \ +.PHONY: base-requirements check-types clean \ compile-requirements detect_changed_source_translations dev-requirements \ docker_auth docker_build docker_push docker_tag docs extract_translations \ guides help lint-imports local-requirements pre-requirements pull \ @@ -23,24 +23,17 @@ clean: ## archive and delete most git-ignored files tar xf $(PRIVATE_FILES) rm $(PRIVATE_FILES) -SWAGGER = docs/swagger.yaml +SWAGGER = docs/lms-openapi.yaml -docs: api-docs guides technical-docs ## build all the developer documentation for this repository +docs: guides technical-docs ## build all the developer documentation for this repository swagger: ## generate the swagger.yaml file DJANGO_SETTINGS_MODULE=docs.docs_settings python manage.py lms generate_swagger --generator-class=edx_api_doc_tools.ApiSchemaGenerator -o $(SWAGGER) -api-docs-sphinx: swagger ## generate the sphinx source files for api-docs - rm -f docs/api/gen/* - python docs/sw2sphinxopenapi.py $(SWAGGER) docs/api/gen - -api-docs: api-docs-sphinx ## build the REST api docs - cd docs/api; make html - technical-docs: ## build the technical docs $(MAKE) -C docs/technical html -guides: ## build the developer guide docs +guides: swagger ## build the developer guide docs cd docs/guides; make clean html extract_translations: ## extract localizable strings from sources diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index cbaf4b8f80..8b5ad70302 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -1,31 +1,33 @@ """ Helper methods for Studio views. """ - +from __future__ import annotations +import logging import urllib from lxml import etree +from mimetypes import guess_type +from attrs import frozen, Factory from django.utils.translation import gettext as _ -from opaque_keys.edx.keys import UsageKey +from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey from opaque_keys.edx.locator import DefinitionLocator, LocalId from xblock.core import XBlock from xblock.fields import ScopeIds from xblock.runtime import IdGenerator +from xmodule.contentstore.content import StaticContent +from xmodule.contentstore.django import contentstore +from xmodule.exceptions import NotFoundError from xmodule.modulestore.django import modulestore -# from cms.djangoapps.contentstore.views.preview import _load_preview_block from cms.djangoapps.models.settings.course_grading import CourseGradingModel from common.djangoapps.student import auth from common.djangoapps.student.roles import CourseCreatorRole, OrgContentCreatorRole - -try: - # Technically this is a django app plugin, so we should not error if it's not installed: - import openedx.core.djangoapps.content_staging.api as content_staging_api -except ImportError: - content_staging_api = None +import openedx.core.djangoapps.content_staging.api as content_staging_api from .utils import reverse_course_url, reverse_library_url, reverse_usage_url +log = logging.getLogger(__name__) + # Note: Grader types are used throughout the platform but most usages are simply in-line # strings. In addition, new grader types can be defined on the fly anytime one is needed # (because they're just strings). This dict is an attempt to constrain the sprawl in Studio. @@ -210,13 +212,24 @@ class ImportIdGenerator(IdGenerator): return DefinitionLocator(block_type, LocalId(block_type)) -def import_staged_content_from_user_clipboard(parent_key: UsageKey, request): +@frozen +class StaticFileNotices: + """ Information about what static files were updated (or not) when pasting content into another course """ + new_files: list[str] = Factory(list) + conflicting_files: list[str] = Factory(list) + error_files: list[str] = Factory(list) + + +def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) -> tuple[XBlock | None, StaticFileNotices]: """ - Import a block (and any children it has) from "staged" OLX. + Import a block (along with its children and any required static assets) from + the "staged" OLX in the user's clipboard. + Does not deal with permissions or REST stuff - do that before calling this. - Returns the newly created block on success or None if the clipboard is - empty. + Returns (1) the newly created block on success or None if the clipboard is + empty, and (2) a summary of changes made to static files in the destination + course. """ from cms.djangoapps.contentstore.views.preview import _load_preview_block @@ -226,9 +239,10 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request): user_clipboard = content_staging_api.get_user_clipboard(request.user.id) if not user_clipboard: # Clipboard is empty or expired/error/loading - return None + return None, StaticFileNotices() block_type = user_clipboard.content.block_type olx_str = content_staging_api.get_staged_content_olx(user_clipboard.content.id) + static_files = content_staging_api.get_staged_content_static_files(user_clipboard.content.id) node = etree.fromstring(olx_str) store = modulestore() with store.bulk_operations(parent_key.course_key): @@ -247,6 +261,11 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request): # 'keys' and uses 'id_generator', but the default XBlock parse_xml ignores 'id_generator' and uses 'keys'. # For children of this block, obviously only id_generator is used. xblock_class = runtime.load_block_type(block_type) + # Note: if we find a case where any XBlock needs access to the block-specific static files that were saved to + # export_fs during copying, we could make them available here via runtime.resources_fs before calling parse_xml. + # However, currently the only known case for that is video block's transcript files, and those will + # automatically be "carried over" to the new XBlock even in a different course because the video ID is the same, + # and VAL will thus make the transcript available. temp_xblock = xblock_class.parse_xml(node, runtime, keys, id_generator) if xblock_class.has_children and temp_xblock.children: raise NotImplementedError("We don't yet support pasting XBlocks with children") @@ -257,7 +276,93 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request): new_xblock = store.update_item(temp_xblock, request.user.id, allow_not_found=True) parent_xblock.children.append(new_xblock.location) store.update_item(parent_xblock, request.user.id) - return new_xblock + + # Now handle static files that need to go into Files & Uploads: + notices = _import_files_into_course( + course_key=parent_key.context_key, + staged_content_id=user_clipboard.content.id, + static_files=static_files, + ) + + return new_xblock, notices + + +def _import_files_into_course( + course_key: CourseKey, + staged_content_id: int, + static_files: list[content_staging_api.StagedContentFileData], +) -> StaticFileNotices: + """ + For the given staged static asset files (which are in "Staged Content" such as the user's clipbaord, but which + need to end up in the course's Files & Uploads page), import them into the destination course, unless they already + exist. + """ + # List of files that were newly added to the destination course + new_files = [] + # List of files that conflicted with identically named files already in the destination course + conflicting_files = [] + # List of files that had an error (shouldn't happen unless we have some kind of bug) + error_files = [] + for file_data_obj in static_files: + if not isinstance(file_data_obj.source_key, AssetKey): + # This static asset was managed by the XBlock and instead of being added to "Files & Uploads", it is stored + # using some other system. We could make it available via runtime.resources_fs during XML parsing, but it's + # not needed here. + continue + # At this point, we know this is a "Files & Uploads" asset that we may need to copy into the course: + try: + result = _import_file_into_course(course_key, staged_content_id, file_data_obj) + if result is True: + new_files.append(file_data_obj.filename) + elif result is None: + pass # This file already exists; no action needed. + else: + conflicting_files.append(file_data_obj.filename) + except Exception: # lint-amnesty, pylint: disable=broad-except + error_files.append(file_data_obj.filename) + log.exception(f"Failed to import Files & Uploads file {file_data_obj.filename}") + return StaticFileNotices( + new_files=new_files, + conflicting_files=conflicting_files, + error_files=error_files, + ) + + +def _import_file_into_course( + course_key: CourseKey, + staged_content_id: int, + file_data_obj: content_staging_api.StagedContentFileData, +) -> bool | None: + """ + Import a single staged static asset file into the course, unless it already exists. + Returns True if it was imported, False if there's a conflict, or None if + the file already existed (no action needed). + """ + filename = file_data_obj.filename + new_key = course_key.make_asset_key("asset", filename) + try: + current_file = contentstore().find(new_key) + except NotFoundError: + current_file = None + if not current_file: + # This static asset should be imported into the new course: + content_type = guess_type(filename)[0] + data = content_staging_api.get_staged_content_static_file_data(staged_content_id, filename) + if data is None: + raise NotFoundError(file_data_obj.source_key) + content = StaticContent(new_key, name=filename, content_type=content_type, data=data) + # If it's an image file, also generate the thumbnail: + thumbnail_content, thumbnail_location = contentstore().generate_thumbnail(content) + if thumbnail_content is not None: + content.thumbnail_location = thumbnail_location + contentstore().save(content) + return True + elif current_file.content_digest == file_data_obj.md5_hash: + # The file already exists and matches exactly, so no action is needed + return None + else: + # There is a conflict with some other file that has the same name. + return False def is_item_in_course_tree(item): diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index db5746f893..8f9674fcc3 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -68,13 +68,8 @@ class TestXBlockI18nService(ModuleStoreTestCase): self.test_language = 'dummy language' self.request = mock.Mock() self.course = CourseFactory.create() - self.field_data = mock.Mock() self.block = BlockFactory(category="pure", parent=self.course) - _prepare_runtime_for_preview( - self.request, - self.block, - self.field_data, - ) + _prepare_runtime_for_preview(self.request, self.block) self.addCleanup(translation.deactivate) def get_block_i18n_service(self, block): diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 144b92d11c..2cde6b3373 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -149,14 +149,13 @@ def preview_layout_asides(block, context, frag, view_name, aside_frag_fns, wrap_ return result -def _prepare_runtime_for_preview(request, block, field_data): +def _prepare_runtime_for_preview(request, block): """ Sets properties in the runtime of the specified block that is required for rendering block previews. request: The active django request block: An XBlock - field_data: Wrapped field data for previews """ course_id = block.location.course_key @@ -199,7 +198,6 @@ def _prepare_runtime_for_preview(request, block, field_data): deprecated_anonymous_user_id = anonymous_id_for_user(request.user, None) services = { - "field-data": field_data, "i18n": XBlockI18nService, 'mako': mako_service, "settings": SettingsService(), @@ -222,7 +220,7 @@ def _prepare_runtime_for_preview(request, block, field_data): # Set up functions to modify the fragment produced by student_view block.runtime.wrappers = wrappers block.runtime.wrappers_asides = wrappers_asides - block.runtime._runtime_services.update(services) # lint-amnesty, pylint: disable=protected-access + block.runtime._services.update(services) # pylint: disable=protected-access # xmodules can check for this attribute during rendering to determine if # they are being rendered for preview (i.e. in Studio) @@ -266,9 +264,7 @@ def _load_preview_block(request: Request, block: XModuleMixin): else: wrapper = partial(LmsFieldData, student_data=student_data) - # wrap the _field_data upfront to pass to _prepare_runtime_for_preview - wrapped_field_data = wrapper(block._field_data) # pylint: disable=protected-access - _prepare_runtime_for_preview(request, block, wrapped_field_data) + _prepare_runtime_for_preview(request, block) block.bind_for_student( request.user.id, diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index d769239c85..d17745654a 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -5,9 +5,9 @@ APIs. """ from opaque_keys.edx.keys import UsageKey from rest_framework.test import APIClient -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import ToyCourseFactory +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 CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/" XBLOCK_ENDPOINT = "/xblock/" @@ -60,3 +60,72 @@ class ClipboardPasteTestCase(ModuleStoreTestCase): assert new_video.youtube_id_1_0 == orig_video.youtube_id_1_0 # The new block should store a reference to where it was copied from assert new_video.copied_from_block == str(video_key) + + def test_paste_with_assets(self): + """ + When pasting into a different course, any required static assets should + be pasted too, unless they already exist in the destination course. + """ + dest_course_key, client = self._setup_course() + # Make sure some files exist in the source course to be copied: + source_course = CourseFactory.create() + upload_file_to_course( + course_key=source_course.id, + contentstore=contentstore(), + source_file='./common/test/data/static/picture1.jpg', + target_filename="picture1.jpg", + ) + upload_file_to_course( + course_key=source_course.id, + contentstore=contentstore(), + source_file='./common/test/data/static/picture2.jpg', + target_filename="picture2.jpg", + ) + source_html = BlockFactory.create( + parent_location=source_course.location, + category="html", + display_name="Some HTML", + data=""" +

+ Picture 1 + Picture 2 +

+ """, + ) + + # Now, to test conflict handling, we also upload a CONFLICTING image to + # the destination course under the same filename. + upload_file_to_course( + course_key=dest_course_key, + contentstore=contentstore(), + # Note this is picture 3, not picture 2, but we save it as picture 2: + source_file='./common/test/data/static/picture3.jpg', + target_filename="picture2.jpg", + ) + + # Now copy the HTML block from the source cost and paste it into the destination: + copy_response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(source_html.location)}, format="json") + assert copy_response.status_code == 200 + + # Paste the video + dest_parent_key = dest_course_key.make_usage_key("vertical", "vertical_test") + paste_response = client.post(XBLOCK_ENDPOINT, { + "parent_locator": str(dest_parent_key), + "staged_content": "clipboard", + }, format="json") + assert paste_response.status_code == 200 + static_file_notices = paste_response.json()["static_file_notices"] + assert static_file_notices == { + "error_files": [], + "new_files": ["picture1.jpg"], + # The new course already had a file named "picture2.jpg" with different md5 hash, so it's a conflict: + "conflicting_files": ["picture2.jpg"], + } + + # Check that the files are as we expect: + source_pic1_hash = contentstore().find(source_course.id.make_asset_key("asset", "picture1.jpg")).content_digest + dest_pic1_hash = contentstore().find(dest_course_key.make_asset_key("asset", "picture1.jpg")).content_digest + assert source_pic1_hash == dest_pic1_hash + 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. diff --git a/cms/djangoapps/contentstore/views/tests/test_preview.py b/cms/djangoapps/contentstore/views/tests/test_preview.py index 0908a2eb8f..0f6786ca62 100644 --- a/cms/djangoapps/contentstore/views/tests/test_preview.py +++ b/cms/djangoapps/contentstore/views/tests/test_preview.py @@ -172,7 +172,6 @@ class GetPreviewHtmlTestCase(ModuleStoreTestCase): self.assertFalse(modulestore().has_changes(modulestore().get_item(block.location))) -@XBlock.needs("field-data") @XBlock.needs("i18n") @XBlock.needs("mako") @XBlock.needs("replace_urls") @@ -204,7 +203,6 @@ class StudioXBlockServiceBindingTest(ModuleStoreTestCase): self.user = UserFactory() self.course = CourseFactory.create() self.request = mock.Mock() - self.field_data = mock.Mock() @XBlock.register_temp_plugin(PureXBlock, identifier='pure') @ddt.data("user", "i18n", "field-data", "teams_configuration", "replace_urls") @@ -213,11 +211,7 @@ class StudioXBlockServiceBindingTest(ModuleStoreTestCase): Tests that the 'user' and 'i18n' services are provided by the Studio runtime. """ block = BlockFactory(category="pure", parent=self.course) - _prepare_runtime_for_preview( - self.request, - block, - self.field_data, - ) + _prepare_runtime_for_preview(self.request, block) service = block.runtime.service(block, expected_service) self.assertIsNotNone(service) @@ -241,14 +235,9 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase): self.request = RequestFactory().get('/dummy-url') self.request.user = self.user self.request.session = {} - self.field_data = mock.Mock() self.contentstore = contentstore() self.block = BlockFactory(category="problem", parent=course) - _prepare_runtime_for_preview( - self.request, - block=self.block, - field_data=mock.Mock(), - ) + _prepare_runtime_for_preview(self.request, block=self.block) self.course = self.store.get_item(course.location) def test_get_user_role(self): @@ -303,11 +292,7 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase): """Test anonymous_user_id on a block which uses per-student anonymous IDs""" # Create the runtime with the flag turned on. block = BlockFactory(category="problem", parent=self.course) - _prepare_runtime_for_preview( - self.request, - block=block, - field_data=mock.Mock(), - ) + _prepare_runtime_for_preview(self.request, block=block) deprecated_anonymous_user_id = ( block.runtime.service(block, 'user').get_current_user().opt_attrs.get(ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID) ) @@ -318,11 +303,7 @@ class CmsModuleSystemShimTest(ModuleStoreTestCase): """Test anonymous_user_id on a block which uses per-course anonymous IDs""" # Create the runtime with the flag turned on. block = BlockFactory(category="lti", parent=self.course) - _prepare_runtime_for_preview( - self.request, - block=block, - field_data=mock.Mock(), - ) + _prepare_runtime_for_preview(self.request, block=block) anonymous_user_id = ( block.runtime.service(block, 'user').get_current_user().opt_attrs.get(ATTR_KEY_ANONYMOUS_USER_ID) diff --git a/cms/djangoapps/contentstore/xblock_services/xblock_service.py b/cms/djangoapps/contentstore/xblock_services/xblock_service.py index 696760a2db..f1fb8eb048 100644 --- a/cms/djangoapps/contentstore/xblock_services/xblock_service.py +++ b/cms/djangoapps/contentstore/xblock_services/xblock_service.py @@ -13,6 +13,7 @@ import logging from datetime import datetime from uuid import uuid4 +from attrs import asdict from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.models import (User) # lint-amnesty, pylint: disable=imported-auth-user @@ -562,7 +563,7 @@ def _create_block(request): if request.json.get("staged_content") == "clipboard": # Paste from the user's clipboard (content_staging app clipboard, not browser clipboard) into 'usage_key': try: - created_xblock = import_staged_content_from_user_clipboard( + created_xblock, notices = import_staged_content_from_user_clipboard( parent_key=usage_key, request=request ) except Exception: # pylint: disable=broad-except @@ -576,12 +577,11 @@ def _create_block(request): return JsonResponse( {"error": _("Your clipboard is empty or invalid.")}, status=400 ) - return JsonResponse( - { - "locator": str(created_xblock.location), - "courseKey": str(created_xblock.location.course_key), - } - ) + return JsonResponse({ + "locator": str(created_xblock.location), + "courseKey": str(created_xblock.location.course_key), + "static_file_notices": asdict(notices), + }) category = request.json["category"] if isinstance(usage_key, LibraryUsageLocator): diff --git a/cms/envs/common.py b/cms/envs/common.py index d3660edc63..4d8d8edfcf 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1533,6 +1533,7 @@ YOUTUBE = { 'TRANSCRIPTS': { 'CAPTION_TRACKS_REGEX': r"captionTracks\"\:\[(?P[^\]]+)", 'YOUTUBE_URL_BASE': 'https://www.youtube.com/watch?v=', + 'ALLOWED_LANGUAGE_CODES': ["en", "en-US", "en-GB"], }, 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 @@ -2725,3 +2726,11 @@ BRAZE_COURSE_ENROLLMENT_CANVAS_ID = '' DISCUSSIONS_INCONTEXT_FEEDBACK_URL = '' DISCUSSIONS_INCONTEXT_LEARNMORE_URL = '' + +OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.content_authoring.staged_content.static_filter_source.v1": { + "pipeline": [ + "openedx.core.djangoapps.content_staging.filters.IgnoreLargeFiles", + ] + } +} diff --git a/cms/static/js/i18n/eo/djangojs.js b/cms/static/js/i18n/eo/djangojs.js index 4cd0f31106..bbc836b794 100644 --- a/cms/static/js/i18n/eo/djangojs.js +++ b/cms/static/js/i18n/eo/djangojs.js @@ -1158,6 +1158,7 @@ "New Password": "N\u00e9w P\u00e4ssw\u00f6rd \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", "New document": "N\u00e9w d\u00f6\u00e7\u00fcm\u00e9nt \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", "New enrollment mode:": "N\u00e9w \u00e9nr\u00f6llm\u00e9nt m\u00f6d\u00e9: \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", + "New files were added to this course's Files & Uploads": "N\u00e9w f\u00efl\u00e9s w\u00e9r\u00e9 \u00e4dd\u00e9d t\u00f6 th\u00efs \u00e7\u00f6\u00fcrs\u00e9's F\u00efl\u00e9s & \u00dbpl\u00f6\u00e4ds \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#", "New window": "N\u00e9w w\u00efnd\u00f6w \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3#", "New {component_type}": "N\u00e9w {component_type} \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c #", "Next": "N\u00e9xt \u2c60'\u03c3\u044f\u0454\u043c \u03b9#", @@ -1617,6 +1618,7 @@ "Skip": "Sk\u00efp \u2c60'\u03c3\u044f\u0454\u043c \u03b9#", "Social Media Links": "S\u00f6\u00e7\u00ef\u00e4l M\u00e9d\u00ef\u00e4 L\u00efnks \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442#", "Some Rights Reserved": "S\u00f6m\u00e9 R\u00efghts R\u00e9s\u00e9rv\u00e9d \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", + "Some errors occurred": "S\u00f6m\u00e9 \u00e9rr\u00f6rs \u00f6\u00e7\u00e7\u00fcrr\u00e9d \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, #", "Some images in this post have been omitted": "S\u00f6m\u00e9 \u00efm\u00e4g\u00e9s \u00efn th\u00efs p\u00f6st h\u00e4v\u00e9 \u00df\u00e9\u00e9n \u00f6m\u00eftt\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 #", "Something has gone wrong ending your exam. Please double-check that the application is running.": "S\u00f6m\u00e9th\u00efng h\u00e4s g\u00f6n\u00e9 wr\u00f6ng \u00e9nd\u00efng \u00fd\u00f6\u00fcr \u00e9x\u00e4m. Pl\u00e9\u00e4s\u00e9 d\u00f6\u00fc\u00dfl\u00e9-\u00e7h\u00e9\u00e7k th\u00e4t th\u00e9 \u00e4ppl\u00ef\u00e7\u00e4t\u00ef\u00f6n \u00efs r\u00fcnn\u00efng. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2#", "Something has gone wrong ending your exam. Please reload the page and start again.": "S\u00f6m\u00e9th\u00efng h\u00e4s g\u00f6n\u00e9 wr\u00f6ng \u00e9nd\u00efng \u00fd\u00f6\u00fcr \u00e9x\u00e4m. Pl\u00e9\u00e4s\u00e9 r\u00e9l\u00f6\u00e4d th\u00e9 p\u00e4g\u00e9 \u00e4nd st\u00e4rt \u00e4g\u00e4\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#", @@ -1785,9 +1787,12 @@ "The following email addresses and/or usernames are invalid:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng \u00e9m\u00e4\u00efl \u00e4ddr\u00e9ss\u00e9s \u00e4nd/\u00f6r \u00fcs\u00e9rn\u00e4m\u00e9s \u00e4r\u00e9 \u00efnv\u00e4l\u00efd: \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 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 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#", + "The following required files were imported to this course:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng r\u00e9q\u00fc\u00efr\u00e9d f\u00efl\u00e9s w\u00e9r\u00e9 \u00efmp\u00f6rt\u00e9d t\u00f6 th\u00efs \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#", "The following users are no longer enrolled in the course:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng \u00fcs\u00e9rs \u00e4r\u00e9 n\u00f6 l\u00f6ng\u00e9r \u00e9nr\u00f6ll\u00e9d \u00efn 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#", "The following warnings were generated:": "Th\u00e9 f\u00f6ll\u00f6w\u00efng w\u00e4rn\u00efngs 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\u044f#", "The general category for this type of assignment, for example, Homework or Midterm Exam. This name is visible to learners.": "Th\u00e9 g\u00e9n\u00e9r\u00e4l \u00e7\u00e4t\u00e9g\u00f6r\u00fd f\u00f6r th\u00efs t\u00fdp\u00e9 \u00f6f \u00e4ss\u00efgnm\u00e9nt, f\u00f6r \u00e9x\u00e4mpl\u00e9, H\u00f6m\u00e9w\u00f6rk \u00f6r M\u00efdt\u00e9rm \u00c9x\u00e4m. Th\u00efs n\u00e4m\u00e9 \u00efs v\u00efs\u00ef\u00dfl\u00e9 t\u00f6 l\u00e9\u00e4rn\u00e9rs. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455#", @@ -2284,6 +2289,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}.": "\u00dd\u00f6\u00fc m\u00e4\u00fd \u00e4ls\u00f6 l\u00f6s\u00e9 \u00e4\u00e7\u00e7\u00e9ss t\u00f6 v\u00e9r\u00eff\u00ef\u00e9d \u00e7\u00e9rt\u00eff\u00ef\u00e7\u00e4t\u00e9s \u00e4nd \u00f6th\u00e9r pr\u00f6gr\u00e4m \u00e7r\u00e9d\u00e9nt\u00ef\u00e4ls l\u00efk\u00e9 M\u00ef\u00e7r\u00f6M\u00e4st\u00e9rs \u00e7\u00e9rt\u00eff\u00ef\u00e7\u00e4t\u00e9s. \u00ccf \u00fd\u00f6\u00fc w\u00e4nt t\u00f6 m\u00e4k\u00e9 \u00e4 \u00e7\u00f6p\u00fd \u00f6f th\u00e9s\u00e9 f\u00f6r \u00fd\u00f6\u00fcr r\u00e9\u00e7\u00f6rds \u00df\u00e9f\u00f6r\u00e9 pr\u00f6\u00e7\u00e9\u00e9d\u00efng w\u00efth d\u00e9l\u00e9t\u00ef\u00f6n, f\u00f6ll\u00f6w th\u00e9 \u00efnstr\u00fc\u00e7t\u00ef\u00f6ns f\u00f6r {htmlStart}pr\u00efnt\u00efng \u00f6r d\u00f6wnl\u00f6\u00e4d\u00efng \u00e4 \u00e7\u00e9rt\u00eff\u00ef\u00e7\u00e4t\u00e9{htmlEnd}. \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\u2202\u03b9\u03c1\u03b9\u0455\u03b9\u00a2\u03b9\u03b7g \u0454\u0142\u03b9\u0442, \u0455\u0454\u2202 \u2202\u03c3 \u0454\u03b9\u03c5\u0455\u043c\u03c3\u2202 \u0442\u0454\u043c\u03c1\u03c3\u044f \u03b9\u03b7\u00a2\u03b9\u2202\u03b9\u2202\u03c5\u03b7\u0442 \u03c5\u0442 \u0142\u03b1\u0432\u03c3\u044f\u0454 \u0454\u0442 \u2202\u03c3\u0142\u03c3\u044f\u0454 \u043c\u03b1g\u03b7\u03b1 \u03b1\u0142\u03b9q\u03c5\u03b1. \u03c5\u0442 \u0454\u03b7\u03b9\u043c \u03b1\u2202 \u043c\u03b9\u03b7\u03b9\u043c \u03bd\u0454\u03b7\u03b9\u03b1\u043c, q\u03c5\u03b9\u0455 \u03b7\u03c3\u0455\u0442\u044f\u03c5\u2202 \u0454\u03c7\u0454\u044f\u00a2\u03b9\u0442\u03b1\u0442\u03b9\u03c3\u03b7 \u03c5\u0142\u0142\u03b1\u043c\u00a2\u03c3 \u0142\u03b1\u0432\u03c3\u044f\u03b9\u0455 \u03b7\u03b9\u0455\u03b9 \u03c5\u0442 \u03b1\u0142\u03b9q\u03c5\u03b9\u03c1 \u0454\u03c7 \u0454\u03b1 \u00a2\u03c3\u043c\u043c\u03c3\u2202\u03c3 \u00a2\u03c3\u03b7\u0455\u0454q\u03c5\u03b1\u0442. \u2202#", "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. ": "\u00dd\u00f6\u00fc m\u00e4\u00fd \u00df\u00e9 \u00e4\u00dfl\u00e9 t\u00f6 \u00e7\u00f6mpl\u00e9t\u00e9 th\u00e9 \u00efm\u00e4g\u00e9 \u00e7\u00e4pt\u00fcr\u00e9 pr\u00f6\u00e7\u00e9d\u00fcr\u00e9 w\u00efth\u00f6\u00fct \u00e4ss\u00efst\u00e4n\u00e7\u00e9, \u00df\u00fct \u00eft m\u00e4\u00fd t\u00e4k\u00e9 \u00e4 \u00e7\u00f6\u00fcpl\u00e9 \u00f6f s\u00fc\u00dfm\u00efss\u00ef\u00f6n \u00e4tt\u00e9mpts t\u00f6 g\u00e9t th\u00e9 \u00e7\u00e4m\u00e9r\u00e4 p\u00f6s\u00eft\u00ef\u00f6n\u00efng r\u00efght. \u00d6pt\u00efm\u00e4l \u00e7\u00e4m\u00e9r\u00e4 p\u00f6s\u00eft\u00ef\u00f6n\u00efng v\u00e4r\u00ef\u00e9s w\u00efth \u00e9\u00e4\u00e7h \u00e7\u00f6mp\u00fct\u00e9r, \u00df\u00fct g\u00e9n\u00e9r\u00e4ll\u00fd th\u00e9 \u00df\u00e9st p\u00f6s\u00eft\u00ef\u00f6n f\u00f6r \u00e4 h\u00e9\u00e4dsh\u00f6t \u00efs \u00e4ppr\u00f6x\u00efm\u00e4t\u00e9l\u00fd 12-18 \u00efn\u00e7h\u00e9s (30-45 \u00e7\u00e9nt\u00efm\u00e9t\u00e9rs) fr\u00f6m th\u00e9 \u00e7\u00e4m\u00e9r\u00e4, w\u00efth \u00fd\u00f6\u00fcr h\u00e9\u00e4d \u00e7\u00e9nt\u00e9r\u00e9d r\u00e9l\u00e4t\u00efv\u00e9 t\u00f6 th\u00e9 \u00e7\u00f6mp\u00fct\u00e9r s\u00e7r\u00e9\u00e9n. #", "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. ": "\u00dd\u00f6\u00fc m\u00e4\u00fd \u00df\u00e9 \u00e4\u00dfl\u00e9 t\u00f6 \u00e7\u00f6mpl\u00e9t\u00e9 th\u00e9 \u00efm\u00e4g\u00e9 \u00e7\u00e4pt\u00fcr\u00e9 pr\u00f6\u00e7\u00e9d\u00fcr\u00e9 w\u00efth\u00f6\u00fct \u00e4ss\u00efst\u00e4n\u00e7\u00e9, \u00df\u00fct \u00eft m\u00e4\u00fd t\u00e4k\u00e9 \u00e4 \u00e7\u00f6\u00fcpl\u00e9 \u00f6f s\u00fc\u00dfm\u00efss\u00ef\u00f6n \u00e4tt\u00e9mpts t\u00f6 g\u00e9t th\u00e9 \u00e7\u00e4m\u00e9r\u00e4 p\u00f6s\u00eft\u00ef\u00f6n\u00efng r\u00efght. \u00d6pt\u00efm\u00e4l \u00e7\u00e4m\u00e9r\u00e4 p\u00f6s\u00eft\u00ef\u00f6n\u00efng v\u00e4r\u00ef\u00e9s w\u00efth \u00e9\u00e4\u00e7h \u00e7\u00f6mp\u00fct\u00e9r, \u00df\u00fct g\u00e9n\u00e9r\u00e4ll\u00fd, th\u00e9 \u00df\u00e9st p\u00f6s\u00eft\u00ef\u00f6n f\u00f6r \u00e4 ph\u00f6t\u00f6 \u00f6f \u00e4n \u00ccD \u00e7\u00e4rd \u00efs 8-12 \u00efn\u00e7h\u00e9s (20-30 \u00e7\u00e9nt\u00efm\u00e9t\u00e9rs) fr\u00f6m th\u00e9 \u00e7\u00e4m\u00e9r\u00e4, w\u00efth th\u00e9 \u00ccD \u00e7\u00e4rd \u00e7\u00e9nt\u00e9r\u00e9d r\u00e9l\u00e4t\u00efv\u00e9 t\u00f6 th\u00e9 \u00e7\u00e4m\u00e9r\u00e4. #", + "You may need to update a file(s) manually": "\u00dd\u00f6\u00fc m\u00e4\u00fd n\u00e9\u00e9d t\u00f6 \u00fcpd\u00e4t\u00e9 \u00e4 f\u00efl\u00e9(s) m\u00e4n\u00fc\u00e4ll\u00fd \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 #", "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}": "\u00dd\u00f6\u00fc m\u00fcst \u00df\u00e9 \u00f6v\u00e9r 13 t\u00f6 sh\u00e4r\u00e9 \u00e4 f\u00fcll pr\u00f6f\u00efl\u00e9. \u00ccf \u00fd\u00f6\u00fc \u00e4r\u00e9 \u00f6v\u00e9r 13, m\u00e4k\u00e9 s\u00fcr\u00e9 th\u00e4t \u00fd\u00f6\u00fc h\u00e4v\u00e9 sp\u00e9\u00e7\u00eff\u00ef\u00e9d \u00e4 \u00df\u00efrth \u00fd\u00e9\u00e4r \u00f6n th\u00e9 {account_settings_page_link} \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455#", "You must enter a valid email address in order to add a new team member": "\u00dd\u00f6\u00fc m\u00fcst \u00e9nt\u00e9r \u00e4 v\u00e4l\u00efd \u00e9m\u00e4\u00efl \u00e4ddr\u00e9ss \u00efn \u00f6rd\u00e9r t\u00f6 \u00e4dd \u00e4 n\u00e9w t\u00e9\u00e4m m\u00e9m\u00df\u00e9r \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 #", "You must have at least one undroppable <%- types %> assignment.": "\u00dd\u00f6\u00fc m\u00fcst h\u00e4v\u00e9 \u00e4t l\u00e9\u00e4st \u00f6n\u00e9 \u00fcndr\u00f6pp\u00e4\u00dfl\u00e9 <%- types %> \u00e4ss\u00efgnm\u00e9nt. \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#", diff --git a/cms/static/js/i18n/rtl/djangojs.js b/cms/static/js/i18n/rtl/djangojs.js index a3178df133..371b67dc0f 100644 --- a/cms/static/js/i18n/rtl/djangojs.js +++ b/cms/static/js/i18n/rtl/djangojs.js @@ -1082,6 +1082,7 @@ "New Password": "N\u01dd\u028d \u2c63\u0250ss\u028d\u00f8\u0279d", "New document": "N\u01dd\u028d d\u00f8\u0254n\u026f\u01ddn\u0287", "New enrollment mode:": "N\u01dd\u028d \u01ddn\u0279\u00f8ll\u026f\u01ddn\u0287 \u026f\u00f8d\u01dd:", + "New files were added to this course's Files & Uploads": "N\u01dd\u028d \u025f\u1d09l\u01dds \u028d\u01dd\u0279\u01dd \u0250dd\u01ddd \u0287\u00f8 \u0287\u0265\u1d09s \u0254\u00f8n\u0279s\u01dd's F\u1d09l\u01dds & \u0244dl\u00f8\u0250ds", "New window": "N\u01dd\u028d \u028d\u1d09nd\u00f8\u028d", "New {component_type}": "N\u01dd\u028d {component_type}", "Next": "N\u01ddx\u0287", @@ -1497,6 +1498,7 @@ "Skip": "S\u029e\u1d09d", "Social Media Links": "S\u00f8\u0254\u1d09\u0250l M\u01ddd\u1d09\u0250 \u0141\u1d09n\u029es", "Some Rights Reserved": "S\u00f8\u026f\u01dd \u024c\u1d09\u0183\u0265\u0287s \u024c\u01dds\u01dd\u0279\u028c\u01ddd", + "Some errors occurred": "S\u00f8\u026f\u01dd \u01dd\u0279\u0279\u00f8\u0279s \u00f8\u0254\u0254n\u0279\u0279\u01ddd", "Some images in this post have been omitted": "S\u00f8\u026f\u01dd \u1d09\u026f\u0250\u0183\u01dds \u1d09n \u0287\u0265\u1d09s d\u00f8s\u0287 \u0265\u0250\u028c\u01dd b\u01dd\u01ddn \u00f8\u026f\u1d09\u0287\u0287\u01ddd", "Something went wrong changing this enrollment. Please try again.": "S\u00f8\u026f\u01dd\u0287\u0265\u1d09n\u0183 \u028d\u01ddn\u0287 \u028d\u0279\u00f8n\u0183 \u0254\u0265\u0250n\u0183\u1d09n\u0183 \u0287\u0265\u1d09s \u01ddn\u0279\u00f8ll\u026f\u01ddn\u0287. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Something went wrong. Please try again later.": "S\u00f8\u026f\u01dd\u0287\u0265\u1d09n\u0183 \u028d\u01ddn\u0287 \u028d\u0279\u00f8n\u0183. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n l\u0250\u0287\u01dd\u0279.", @@ -1654,9 +1656,12 @@ "The file you are trying to upload is too large.": "\u0641\u0627\u062b \u0628\u0647\u0645\u062b \u063a\u062e\u0639 \u0634\u0642\u062b \u0641\u0642\u063a\u0647\u0631\u0644 \u0641\u062e \u0639\u062d\u0645\u062e\u0634\u064a \u0647\u0633 \u0641\u062e\u062e \u0645\u0634\u0642\u0644\u062b.", "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 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:", + "The following required files were imported to this course:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u0279\u01ddbn\u1d09\u0279\u01ddd \u025f\u1d09l\u01dds \u028d\u01dd\u0279\u01dd \u1d09\u026fd\u00f8\u0279\u0287\u01ddd \u0287\u00f8 \u0287\u0265\u1d09s \u0254\u00f8n\u0279s\u01dd:", "The following users are no longer enrolled in the course:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 ns\u01dd\u0279s \u0250\u0279\u01dd n\u00f8 l\u00f8n\u0183\u01dd\u0279 \u01ddn\u0279\u00f8ll\u01ddd \u1d09n \u0287\u0265\u01dd \u0254\u00f8n\u0279s\u01dd:", "The following warnings were generated:": "\u0166\u0265\u01dd \u025f\u00f8ll\u00f8\u028d\u1d09n\u0183 \u028d\u0250\u0279n\u1d09n\u0183s \u028d\u01dd\u0279\u01dd \u0183\u01ddn\u01dd\u0279\u0250\u0287\u01ddd:", "The general category for this type of assignment, for example, Homework or Midterm Exam. This name is visible to learners.": "\u0166\u0265\u01dd \u0183\u01ddn\u01dd\u0279\u0250l \u0254\u0250\u0287\u01dd\u0183\u00f8\u0279\u028e \u025f\u00f8\u0279 \u0287\u0265\u1d09s \u0287\u028ed\u01dd \u00f8\u025f \u0250ss\u1d09\u0183n\u026f\u01ddn\u0287, \u025f\u00f8\u0279 \u01ddx\u0250\u026fdl\u01dd, \u0126\u00f8\u026f\u01dd\u028d\u00f8\u0279\u029e \u00f8\u0279 M\u1d09d\u0287\u01dd\u0279\u026f \u0246x\u0250\u026f. \u0166\u0265\u1d09s n\u0250\u026f\u01dd \u1d09s \u028c\u1d09s\u1d09bl\u01dd \u0287\u00f8 l\u01dd\u0250\u0279n\u01dd\u0279s.", @@ -2107,6 +2112,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}.": "\u024e\u00f8n \u026f\u0250\u028e \u0250ls\u00f8 l\u00f8s\u01dd \u0250\u0254\u0254\u01ddss \u0287\u00f8 \u028c\u01dd\u0279\u1d09\u025f\u1d09\u01ddd \u0254\u01dd\u0279\u0287\u1d09\u025f\u1d09\u0254\u0250\u0287\u01dds \u0250nd \u00f8\u0287\u0265\u01dd\u0279 d\u0279\u00f8\u0183\u0279\u0250\u026f \u0254\u0279\u01ddd\u01ddn\u0287\u1d09\u0250ls l\u1d09\u029e\u01dd M\u1d09\u0254\u0279\u00f8M\u0250s\u0287\u01dd\u0279s \u0254\u01dd\u0279\u0287\u1d09\u025f\u1d09\u0254\u0250\u0287\u01dds. \u0197\u025f \u028e\u00f8n \u028d\u0250n\u0287 \u0287\u00f8 \u026f\u0250\u029e\u01dd \u0250 \u0254\u00f8d\u028e \u00f8\u025f \u0287\u0265\u01dds\u01dd \u025f\u00f8\u0279 \u028e\u00f8n\u0279 \u0279\u01dd\u0254\u00f8\u0279ds b\u01dd\u025f\u00f8\u0279\u01dd d\u0279\u00f8\u0254\u01dd\u01ddd\u1d09n\u0183 \u028d\u1d09\u0287\u0265 d\u01ddl\u01dd\u0287\u1d09\u00f8n, \u025f\u00f8ll\u00f8\u028d \u0287\u0265\u01dd \u1d09ns\u0287\u0279n\u0254\u0287\u1d09\u00f8ns \u025f\u00f8\u0279 {htmlStart}d\u0279\u1d09n\u0287\u1d09n\u0183 \u00f8\u0279 d\u00f8\u028dnl\u00f8\u0250d\u1d09n\u0183 \u0250 \u0254\u01dd\u0279\u0287\u1d09\u025f\u1d09\u0254\u0250\u0287\u01dd{htmlEnd}.", "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. ": "\u024e\u00f8n \u026f\u0250\u028e b\u01dd \u0250bl\u01dd \u0287\u00f8 \u0254\u00f8\u026fdl\u01dd\u0287\u01dd \u0287\u0265\u01dd \u1d09\u026f\u0250\u0183\u01dd \u0254\u0250d\u0287n\u0279\u01dd d\u0279\u00f8\u0254\u01dddn\u0279\u01dd \u028d\u1d09\u0287\u0265\u00f8n\u0287 \u0250ss\u1d09s\u0287\u0250n\u0254\u01dd, bn\u0287 \u1d09\u0287 \u026f\u0250\u028e \u0287\u0250\u029e\u01dd \u0250 \u0254\u00f8ndl\u01dd \u00f8\u025f snb\u026f\u1d09ss\u1d09\u00f8n \u0250\u0287\u0287\u01dd\u026fd\u0287s \u0287\u00f8 \u0183\u01dd\u0287 \u0287\u0265\u01dd \u0254\u0250\u026f\u01dd\u0279\u0250 d\u00f8s\u1d09\u0287\u1d09\u00f8n\u1d09n\u0183 \u0279\u1d09\u0183\u0265\u0287. \u00d8d\u0287\u1d09\u026f\u0250l \u0254\u0250\u026f\u01dd\u0279\u0250 d\u00f8s\u1d09\u0287\u1d09\u00f8n\u1d09n\u0183 \u028c\u0250\u0279\u1d09\u01dds \u028d\u1d09\u0287\u0265 \u01dd\u0250\u0254\u0265 \u0254\u00f8\u026fdn\u0287\u01dd\u0279, bn\u0287 \u0183\u01ddn\u01dd\u0279\u0250ll\u028e \u0287\u0265\u01dd b\u01dds\u0287 d\u00f8s\u1d09\u0287\u1d09\u00f8n \u025f\u00f8\u0279 \u0250 \u0265\u01dd\u0250ds\u0265\u00f8\u0287 \u1d09s \u0250dd\u0279\u00f8x\u1d09\u026f\u0250\u0287\u01ddl\u028e 12-18 \u1d09n\u0254\u0265\u01dds (30-45 \u0254\u01ddn\u0287\u1d09\u026f\u01dd\u0287\u01dd\u0279s) \u025f\u0279\u00f8\u026f \u0287\u0265\u01dd \u0254\u0250\u026f\u01dd\u0279\u0250, \u028d\u1d09\u0287\u0265 \u028e\u00f8n\u0279 \u0265\u01dd\u0250d \u0254\u01ddn\u0287\u01dd\u0279\u01ddd \u0279\u01ddl\u0250\u0287\u1d09\u028c\u01dd \u0287\u00f8 \u0287\u0265\u01dd \u0254\u00f8\u026fdn\u0287\u01dd\u0279 s\u0254\u0279\u01dd\u01ddn. ", "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. ": "\u024e\u00f8n \u026f\u0250\u028e b\u01dd \u0250bl\u01dd \u0287\u00f8 \u0254\u00f8\u026fdl\u01dd\u0287\u01dd \u0287\u0265\u01dd \u1d09\u026f\u0250\u0183\u01dd \u0254\u0250d\u0287n\u0279\u01dd d\u0279\u00f8\u0254\u01dddn\u0279\u01dd \u028d\u1d09\u0287\u0265\u00f8n\u0287 \u0250ss\u1d09s\u0287\u0250n\u0254\u01dd, bn\u0287 \u1d09\u0287 \u026f\u0250\u028e \u0287\u0250\u029e\u01dd \u0250 \u0254\u00f8ndl\u01dd \u00f8\u025f snb\u026f\u1d09ss\u1d09\u00f8n \u0250\u0287\u0287\u01dd\u026fd\u0287s \u0287\u00f8 \u0183\u01dd\u0287 \u0287\u0265\u01dd \u0254\u0250\u026f\u01dd\u0279\u0250 d\u00f8s\u1d09\u0287\u1d09\u00f8n\u1d09n\u0183 \u0279\u1d09\u0183\u0265\u0287. \u00d8d\u0287\u1d09\u026f\u0250l \u0254\u0250\u026f\u01dd\u0279\u0250 d\u00f8s\u1d09\u0287\u1d09\u00f8n\u1d09n\u0183 \u028c\u0250\u0279\u1d09\u01dds \u028d\u1d09\u0287\u0265 \u01dd\u0250\u0254\u0265 \u0254\u00f8\u026fdn\u0287\u01dd\u0279, bn\u0287 \u0183\u01ddn\u01dd\u0279\u0250ll\u028e, \u0287\u0265\u01dd b\u01dds\u0287 d\u00f8s\u1d09\u0287\u1d09\u00f8n \u025f\u00f8\u0279 \u0250 d\u0265\u00f8\u0287\u00f8 \u00f8\u025f \u0250n \u0197\u0110 \u0254\u0250\u0279d \u1d09s 8-12 \u1d09n\u0254\u0265\u01dds (20-30 \u0254\u01ddn\u0287\u1d09\u026f\u01dd\u0287\u01dd\u0279s) \u025f\u0279\u00f8\u026f \u0287\u0265\u01dd \u0254\u0250\u026f\u01dd\u0279\u0250, \u028d\u1d09\u0287\u0265 \u0287\u0265\u01dd \u0197\u0110 \u0254\u0250\u0279d \u0254\u01ddn\u0287\u01dd\u0279\u01ddd \u0279\u01ddl\u0250\u0287\u1d09\u028c\u01dd \u0287\u00f8 \u0287\u0265\u01dd \u0254\u0250\u026f\u01dd\u0279\u0250. ", + "You may need to update a file(s) manually": "\u024e\u00f8n \u026f\u0250\u028e n\u01dd\u01ddd \u0287\u00f8 ndd\u0250\u0287\u01dd \u0250 \u025f\u1d09l\u01dd(s) \u026f\u0250nn\u0250ll\u028e", "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}": "\u024e\u00f8n \u026fns\u0287 b\u01dd \u00f8\u028c\u01dd\u0279 13 \u0287\u00f8 s\u0265\u0250\u0279\u01dd \u0250 \u025fnll d\u0279\u00f8\u025f\u1d09l\u01dd. \u0197\u025f \u028e\u00f8n \u0250\u0279\u01dd \u00f8\u028c\u01dd\u0279 13, \u026f\u0250\u029e\u01dd sn\u0279\u01dd \u0287\u0265\u0250\u0287 \u028e\u00f8n \u0265\u0250\u028c\u01dd sd\u01dd\u0254\u1d09\u025f\u1d09\u01ddd \u0250 b\u1d09\u0279\u0287\u0265 \u028e\u01dd\u0250\u0279 \u00f8n \u0287\u0265\u01dd {account_settings_page_link}", "You must enter a valid email address in order to add a new team member": "\u024e\u00f8n \u026fns\u0287 \u01ddn\u0287\u01dd\u0279 \u0250 \u028c\u0250l\u1d09d \u01dd\u026f\u0250\u1d09l \u0250dd\u0279\u01ddss \u1d09n \u00f8\u0279d\u01dd\u0279 \u0287\u00f8 \u0250dd \u0250 n\u01dd\u028d \u0287\u01dd\u0250\u026f \u026f\u01dd\u026fb\u01dd\u0279", "You must have at least one undroppable <%- types %> assignment.": "\u024e\u00f8n \u026fns\u0287 \u0265\u0250\u028c\u01dd \u0250\u0287 l\u01dd\u0250s\u0287 \u00f8n\u01dd nnd\u0279\u00f8dd\u0250bl\u01dd <%- types %> \u0250ss\u1d09\u0183n\u026f\u01ddn\u0287.", diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 0586cffc3e..b1ec303bd5 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -6,10 +6,12 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page 'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock', 'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal', 'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor', - 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils'], + 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils', + 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', +], function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor, - ContainerSubviews, UnitOutlineView, XBlockUtils) { + ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView) { 'use strict'; var XBlockContainerPage = BasePage.extend({ @@ -255,10 +257,60 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView staged_content: "clipboard", }).then((data) => { this.onNewXBlock(placeholderElement, scrollOffset, false, data); + return data; }).fail(() => { // Remove the placeholder if the paste failed placeholderElement.remove(); }); + }).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.Confirmation({ + title: gettext("New files were added to this course's Files & Uploads"), + message: ( + gettext("The following required files were imported to this course:") + + " " + newFiles.join(", ") + ), + closeIcon: true, + })); + } + 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); + } }); }, diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 39efc419ff..57e40c35ff 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -125,9 +125,16 @@ ${_("Pages & Resources")} % endif + %if not files_uploads_mfe_enabled: + %endif + %if files_uploads_mfe_enabled: + + %endif % if not pages_and_resources_mfe_enabled: % endif % if certificates_url: @@ -222,12 +229,26 @@