diff --git a/Makefile b/Makefile index bc5a79712e29..55252bf2aff0 100644 --- a/Makefile +++ b/Makefile @@ -37,8 +37,20 @@ technical-docs: ## build the technical docs guides: swagger ## build the developer guide docs cd docs/guides; make clean html +# (IS_OPENEDX_TRANSLATIONS_WORKFLOW) is set to "yes" in the `extract-translation-source-files` GitHub actions +# workflow on the `openedx-translations` repository. See (extract translation source files) step here: +# https://github.com/openedx/openedx-translations/blob/main/.github/workflows/extract-translation-source-files.yml +# Related doc: https://docs.openedx.org/en/latest/developers/how-tos/enable-translations-new-repo.html +ifeq ($(IS_OPENEDX_TRANSLATIONS_WORKFLOW),yes) extract_translations: ## extract localizable strings from sources - i18n_tool extract -v + i18n_tool extract --no-segment -v + cd conf/locale/en/LC_MESSAGES && msgcat djangojs.po underscore.po -o djangojs.po + cd conf/locale/en/LC_MESSAGES && msgcat django.po wiki.po edx_proctoring_proctortrack.po mako.po -o django.po + cd conf/locale/en/LC_MESSAGES && rm wiki.po edx_proctoring_proctortrack.po mako.po underscore.po +else +extract_translations: ## extract localizable strings from sources + i18n_tool extract -v; +endif push_translations: ## push source strings to Transifex for translation i18n_tool transifex push @@ -141,7 +153,7 @@ upgrade: ## update the pip requirements files to use the latest releases satisf $(MAKE) compile-requirements COMPILE_OPTS="--upgrade" upgrade-package: ## update just one package to the latest usable release - @test -n "$(package)" || { echo "\nUsage: make upgrade_package package=...\n"; exit 1; } + @test -n "$(package)" || { echo "\nUsage: make upgrade-package package=...\n"; exit 1; } $(MAKE) compile-requirements COMPILE_OPTS="--upgrade-package $(package)" check-types: ## run static type-checking tests diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py index 852117629b68..89f566f0abfb 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -58,6 +58,20 @@ class VideoModelSerializer(serializers.Serializer): ) +class VideoActiveTranscriptPreferencesSerializer(serializers.Serializer): + """Serializer for a videos active transcript preferences""" + course_id = serializers.CharField() + provider = serializers.CharField() + cielo24_fidelity = serializers.CharField() + cielo24_turnaround = serializers.CharField() + three_play_turnaround = serializers.CharField() + preferred_languages = serializers.ListField( + child=serializers.CharField() + ) + video_source_language = serializers.CharField() + modified = serializers.CharField() + + class CourseVideosSerializer(serializers.Serializer): """Serializer for course videos""" image_upload_url = serializers.CharField() @@ -72,15 +86,16 @@ 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() - active_transcript_preferences = serializers.BooleanField(required=False, allow_null=True) + active_transcript_preferences = VideoActiveTranscriptPreferencesSerializer(required=False, allow_null=True) transcript_credentials = serializers.DictField( - child=serializers.CharField() + child=serializers.BooleanField() ) transcript_available_languages = serializers.ListField( child=serializers.DictField( child=serializers.CharField() ) ) + # transcript_available_languages = serializers.BooleanField(required=False, allow_null=True) video_transcript_settings = VideoTranscriptSettingsSerializer() pagination_context = serializers.DictField( child=serializers.CharField(), diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 1eb70347399c..b82605f8933f 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -45,7 +45,12 @@ from cms.djangoapps.contentstore.config import waffle from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, get_url, parse_json -from cms.djangoapps.contentstore.utils import delete_course, reverse_course_url, reverse_url +from cms.djangoapps.contentstore.utils import ( + delete_course, + reverse_course_url, + reverse_url, + get_taxonomy_tags_widget_url, +) from cms.djangoapps.contentstore.views.component import ADVANCED_COMPONENT_TYPES from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager @@ -1410,12 +1415,16 @@ def test_course_overview_view_with_course(self): 'assets_handler', course.location.course_key ) + + taxonomy_tags_widget_url = get_taxonomy_tags_widget_url(course.id) + self.assertContains( resp, - '
'.format( # lint-amnesty, pylint: disable=line-too-long + '
'.format( # lint-amnesty, pylint: disable=line-too-long locator=str(course.location), course_key=str(course.id), assets_url=assets_url, + taxonomy_tags_widget_url=taxonomy_tags_widget_url, ), status_code=200, html=True diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 48fc2c962cf1..ba227ecbec0e 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -46,7 +46,6 @@ ) from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from openedx.core import toggles as core_toggles -from openedx.core.djangoapps.course_apps.toggles import proctoring_settings_modal_view_enabled from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration @@ -246,10 +245,7 @@ def get_proctored_exam_settings_url(course_locator) -> str: mfe_base_url = get_course_authoring_url(course_locator) course_mfe_url = f'{mfe_base_url}/course/{course_locator}' if mfe_base_url: - if proctoring_settings_modal_view_enabled(course_locator): - proctored_exam_settings_url = f'{course_mfe_url}/pages-and-resources/proctoring/settings' - else: - proctored_exam_settings_url = f'{course_mfe_url}/proctored-exam-settings' + proctored_exam_settings_url = f'{course_mfe_url}/pages-and-resources/proctoring/settings' return proctored_exam_settings_url @@ -446,6 +442,21 @@ def get_taxonomy_list_url(): return taxonomy_list_url +def get_taxonomy_tags_widget_url(course_locator) -> str: + """ + Gets course authoring microfrontend URL for taxonomy tags drawer widget view. + + The `content_id` needs to be appended to the end of the URL when using it. + """ + taxonomy_tags_widget_url = None + # Uses the same waffle flag as taxonomy list page + if use_tagging_taxonomy_list_page(): + mfe_base_url = get_course_authoring_url(course_locator) + if mfe_base_url: + taxonomy_tags_widget_url = f'{mfe_base_url}/tagging/components/widget/' + return taxonomy_tags_widget_url + + def course_import_olx_validation_is_enabled(): """ Check if course olx validation is enabled on course import. diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index a55bb3db9a53..b42c011c4b20 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -105,6 +105,7 @@ 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, @@ -688,6 +689,7 @@ def course_index(request, course_key): '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), }) diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index 425f97e3751f..dca25695a29d 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -24,6 +24,7 @@ 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 edx_proctoring.api import ( does_backend_support_onboarding, get_exam_by_content_id, @@ -38,7 +39,7 @@ from xblock.fields import Scope from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG -from cms.djangoapps.contentstore.toggles import ENABLE_COPY_PASTE_UNITS +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 @@ -54,6 +55,7 @@ from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE from openedx.core.lib.gating import api as gating_api +from openedx.core.lib.cache_utils import request_cached from openedx.core.toggles import ENTRANCE_EXAMS from xmodule.course_block import ( DEFAULT_START_DATE, @@ -1397,6 +1399,11 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements # 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 + xblock_info["tag_counts_by_unit"] = _get_course_unit_tags(xblock.location.context_key) + xblock_info[ "has_partition_group_components" ] = has_children_visible_to_specific_partition_groups(xblock) @@ -1410,6 +1417,19 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements return xblock_info +@request_cached() +def _get_course_unit_tags(course_key) -> dict: + """ + Get the count of tags that are applied to each unit (vertical) in this course, as a dict. + """ + if not course_key.is_course: + return {} # Unsupported key type, e.g. a library + # 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) + + def _was_xblock_ever_exam_linked_with_external(course, xblock): """ Determine whether this XBlock is or was ever configured as an external proctored exam. diff --git a/cms/envs/common.py b/cms/envs/common.py index c7236bec6c9c..e8beed8c6235 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2852,3 +2852,5 @@ def _should_send_xblock_events(settings): # This affects the Authoring API swagger docs but not the legacy swagger docs under /api-docs/. REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] = 'drf_spectacular.openapi.AutoSchema' + +BEAMER_PRODUCT_ID = "" diff --git a/cms/envs/production.py b/cms/envs/production.py index 520ad7f3bdee..d0a846d3da21 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -708,3 +708,5 @@ def get_env_setting(setting): {'url': f'https://{CMS_BASE}/api/contentstore', 'description': 'Local'}, ], } + +BEAMER_PRODUCT_ID = ENV_TOKENS.get('BEAMER_PRODUCT_ID', BEAMER_PRODUCT_ID) diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index 0351f2e175d1..2faccde97e36 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -458,6 +458,43 @@ function( event.stopPropagation(); }, + closeManageTagsDrawer(drawer, drawerCover) { + $(drawerCover).css('display', 'none'); + $(drawer).empty(); + $(drawer).css('display', 'none'); + $('body').removeClass('drawer-open'); + }, + + openManageTagsDrawer(event) { + const drawer = document.querySelector("#manage-tags-drawer"); + const drawerCover = document.querySelector(".drawer-cover") + const article = document.querySelector('[data-taxonomy-tags-widget-url]'); + const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url'); + const contentId = this.model.get('id'); + + // Add handler to close drawer when dark background is clicked + $(drawerCover).click(function() { + this.closeManageTagsDrawer(drawer, drawerCover); + }.bind(this)); + + // Add event listen to close drawer when close button is clicked from within the Iframe + window.addEventListener("message", function (event) { + if (event.data === 'closeManageTagsDrawer') { + this.closeManageTagsDrawer(drawer, drawerCover) + } + }.bind(this)); + + $(drawerCover).css('display', 'block'); + // xss-lint: disable=javascript-jquery-html + $(drawer).html( + `` + ); + $(drawer).css('display', 'block'); + + // Prevent background from being scrollable when drawer is open + $('body').addClass('drawer-open'); + }, + addButtonActions: function(element) { XBlockOutlineView.prototype.addButtonActions.apply(this, arguments); element.find('.configure-button').click(function(event) { @@ -478,6 +515,10 @@ function( event.preventDefault(); this.copyXBlock(); }); + element.find('.manage-tags-button').click((event) => { + event.preventDefault(); + this.openManageTagsDrawer(); + }); element.find('.paste-component-button').click((event) => { event.preventDefault(); this.pasteUnit(event); diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index 9ded8d66e91f..4e826025b971 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -114,6 +114,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldE staffOnlyMessage: this.model.get('staff_only_message'), course: course, enableCopyPasteUnits: this.model.get("enable_copy_paste_units"), // ENABLE_COPY_PASTE_UNITS waffle flag + useTaggingTaxonomyListPage: this.model.get("use_tagging_taxonomy_list_page"), // ENABLE_TAGGING_TAXONOMY_LIST_PAGE waffle flag }; }, diff --git a/cms/static/sass/_build-v1.scss b/cms/static/sass/_build-v1.scss index fefd095e21a7..178f6b167473 100644 --- a/cms/static/sass/_build-v1.scss +++ b/cms/static/sass/_build-v1.scss @@ -55,6 +55,7 @@ @import 'elements/uploaded-assets'; // layout for asset tables @import 'elements/creative-commons'; @import 'elements/tooltip'; +@import 'elements/drawer'; // +Base - Specific Views // ==================== diff --git a/cms/static/sass/elements/_drawer.scss b/cms/static/sass/elements/_drawer.scss new file mode 100644 index 000000000000..c18073be9864 --- /dev/null +++ b/cms/static/sass/elements/_drawer.scss @@ -0,0 +1,30 @@ +// studio - elements - side drawers +// ==================== + +.drawer-cover { + @extend %ui-depth3; + + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); +} + +.drawer { + @extend %ui-depth4; + + display: none; + position: fixed; + top: 0; + right: 0; + width: 33.33vw; + height: 100vh; + background-color: $gray-l4; +} + +body.drawer-open { + overflow: hidden; +} diff --git a/cms/templates/base.html b/cms/templates/base.html index e910cac83969..6907161fff54 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -188,4 +188,10 @@ <%include file="widgets/segment-io-footer.html" /> + + diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 0d1773e7c188..e5959867be86 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -281,7 +281,7 @@

${_("Page Actions")}

assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_locator.course_key)}) %>

${_("Course Outline")}

-
+
@@ -321,4 +321,7 @@

${_("Changing the content learners see")}

+ +
+
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index fa3d562196b4..baf802a29112 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -7,6 +7,7 @@ var hasPartitionGroups = xblockInfo.get('has_partition_group_components'); var userPartitionInfo = xblockInfo.get('user_partition_info'); var selectedGroupsLabel = userPartitionInfo['selected_groups_label']; var selectedPartitionIndex = userPartitionInfo['selected_partition_index']; +var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockInfo.get('id')] || 0; var statusMessages = []; var messageType; @@ -169,6 +170,17 @@ if (is_proctored_exam) { <% } %> + + <% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage && tagsCount > 0) { %> +
  • + + + <%- tagsCount %> + <%- gettext('Manage Tags') %> + +
  • + <% } %> + <% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %>