diff --git a/cms/lib/xblock/tagging/tagging.py b/cms/lib/xblock/tagging/tagging.py index 80f7cc0f2859..41e41d33fc0c 100644 --- a/cms/lib/xblock/tagging/tagging.py +++ b/cms/lib/xblock/tagging/tagging.py @@ -8,7 +8,6 @@ from xblock.fields import Dict, Scope from common.djangoapps.edxmako.shortcuts import render_to_string -from xmodule.capa_block import ProblemBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.x_module import AUTHOR_VIEW # lint-amnesty, pylint: disable=wrong-import-order _ = lambda text: text @@ -42,6 +41,7 @@ def student_view_aside(self, block, context): # pylint: disable=unused-argument Display the tag selector with specific categories and allowed values, depending on the context. """ + from xmodule.capa_block import ProblemBlock if isinstance(block, ProblemBlock): tags = [] for tag in self.get_available_tags(): diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index b182202eddaf..d44d43ca36d4 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -33,6 +33,8 @@ from django.views.generic import View from edx_django_utils.monitoring import set_custom_attribute, set_custom_attributes_for_course_key from ipware.ip import get_client_ip +from xblock.core import XBlock + from lms.djangoapps.static_template_view.views import render_500 from markupsafe import escape from opaque_keys import InvalidKeyError @@ -1558,6 +1560,10 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True, disable_sta set_custom_attributes_for_course_key(course_key) set_custom_attribute('usage_key', usage_key_string) set_custom_attribute('block_type', usage_key.block_type) + block_class = XBlock.load_class(usage_key.block_type) + if hasattr(block_class, 'is_extracted'): + is_extracted = getattr(block_class, 'is_extracted') + set_custom_attribute('block_extracted', is_extracted) requested_view = request.GET.get('view', 'student_view') if requested_view != 'student_view' and requested_view != 'public_view': # lint-amnesty, pylint: disable=consider-using-in diff --git a/requirements/edx/bundled.in b/requirements/edx/bundled.in index 5a46c710a6d2..a9394b809f55 100644 --- a/requirements/edx/bundled.in +++ b/requirements/edx/bundled.in @@ -47,3 +47,4 @@ ora2>=4.5.0 # Open Response Assessment XBlock xblock-poll # Xblock for polling users xblock-drag-and-drop-v2 # Drag and Drop XBlock xblock-google-drive # XBlock for google docs and calendar +xblocks-contrib # Package having multiple core XBlocks, https://github.com/openedx/xblocks-contrib?tab=readme-ov-file#xblocks-being-moved-here diff --git a/xmodule/annotatable_block.py b/xmodule/annotatable_block.py index cec677f6c5d5..9bd8b13bd937 100644 --- a/xmodule/annotatable_block.py +++ b/xmodule/annotatable_block.py @@ -7,18 +7,20 @@ from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String +from xblocks_contrib.annotatable import AnnotatableBlock as _ExtractedAnnotatableBlock from openedx.core.djangolib.markup import HTML, Text from xmodule.editing_block import EditingMixin from xmodule.raw_block import RawMixin +from xmodule.toggles import USE_EXTRACTED_ANNOTATABLE_BLOCK from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment -from xmodule.xml_block import XmlMixin from xmodule.x_module import ( ResourceTemplates, shim_xmodule_js, XModuleMixin, XModuleToXBlockMixin, ) +from xmodule.xml_block import XmlMixin log = logging.getLogger(__name__) @@ -28,7 +30,7 @@ @XBlock.needs('mako') -class AnnotatableBlock( +class _BuiltInAnnotatableBlock( RawMixin, XmlMixin, EditingMixin, @@ -40,6 +42,8 @@ class AnnotatableBlock( Annotatable XBlock. """ + is_extracted = False + data = String( help=_("XML data for the annotation"), scope=Scope.content, @@ -197,3 +201,9 @@ def studio_view(self, _context): add_webpack_js_to_fragment(fragment, 'AnnotatableBlockEditor') shim_xmodule_js(fragment, self.studio_js_module_name) return fragment + + +AnnotatableBlock = ( + _ExtractedAnnotatableBlock if USE_EXTRACTED_ANNOTATABLE_BLOCK.is_enabled() + else _BuiltInAnnotatableBlock +) diff --git a/xmodule/capa_block.py b/xmodule/capa_block.py index c1c650144b05..8fc7c203a5c5 100644 --- a/xmodule/capa_block.py +++ b/xmodule/capa_block.py @@ -25,7 +25,14 @@ from xblock.core import XBlock from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString, List from xblock.scorable import ScorableXBlockMixin, Score +from xblocks_contrib.problem import ProblemBlock as _ExtractedProblemBlock +from common.djangoapps.xblock_django.constants import ( + ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID, + ATTR_KEY_USER_IS_STAFF, + ATTR_KEY_USER_ID, +) +from openedx.core.djangolib.markup import HTML, Text from xmodule.capa import responsetypes from xmodule.capa.capa_problem import LoncapaProblem, LoncapaSystem from xmodule.capa.inputtypes import Status @@ -36,8 +43,9 @@ from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.graders import ShowCorrectness from xmodule.raw_block import RawMixin -from xmodule.util.sandboxing import SandboxService +from xmodule.toggles import USE_EXTRACTED_PROBLEM_BLOCK from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment +from xmodule.util.sandboxing import SandboxService from xmodule.x_module import ( ResourceTemplates, XModuleMixin, @@ -45,14 +53,7 @@ shim_xmodule_js ) from xmodule.xml_block import XmlMixin -from common.djangoapps.xblock_django.constants import ( - ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID, - ATTR_KEY_USER_IS_STAFF, - ATTR_KEY_USER_ID, -) -from openedx.core.djangolib.markup import HTML, Text from .capa.xqueue_interface import XQueueService - from .fields import Date, ListScoreField, ScoreField, Timedelta from .progress import Progress @@ -134,7 +135,7 @@ def from_json(self, value): @XBlock.needs('sandbox') @XBlock.needs('replace_urls') @XBlock.wants('call_to_action') -class ProblemBlock( +class _BuiltInProblemBlock( ScorableXBlockMixin, RawMixin, XmlMixin, @@ -161,6 +162,8 @@ class ProblemBlock( """ INDEX_CONTENT_TYPE = 'CAPA' + is_extracted = False + resources_dir = None has_score = True @@ -2509,3 +2512,9 @@ def randomization_bin(seed, problem_id): r_hash.update(str(problem_id).encode()) # get the first few digits of the hash, convert to an int, then mod. return int(r_hash.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS + + +ProblemBlock = ( + _ExtractedProblemBlock if USE_EXTRACTED_PROBLEM_BLOCK.is_enabled() + else _BuiltInProblemBlock +) diff --git a/xmodule/discussion_block.py b/xmodule/discussion_block.py index 89e573c07c83..10863d9639c8 100644 --- a/xmodule/discussion_block.py +++ b/xmodule/discussion_block.py @@ -14,14 +14,15 @@ from xblock.fields import UNIQUE_ID, Scope, String from xblock.utils.resources import ResourceLoader from xblock.utils.studio_editable import StudioEditableXBlockMixin +from xblocks_contrib.discussion import DiscussionXBlock as _ExtractedDiscussionXBlock from lms.djangoapps.discussion.django_comment_client.permissions import has_permission from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider from openedx.core.djangolib.markup import HTML, Text from openedx.core.lib.xblock_utils import get_css_dependencies, get_js_dependencies +from xmodule.toggles import USE_EXTRACTED_DISCUSSION_BLOCK from xmodule.xml_block import XmlMixin - log = logging.getLogger(__name__) loader = ResourceLoader(__name__) # pylint: disable=invalid-name @@ -36,10 +37,11 @@ def _(text): @XBlock.needs('user') # pylint: disable=abstract-method @XBlock.needs('i18n') @XBlock.needs('mako') -class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlMixin): # lint-amnesty, pylint: disable=abstract-method +class _BuiltInDiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlMixin): # lint-amnesty, pylint: disable=abstract-method """ Provides a discussion forum that is inline with other content in the courseware. """ + is_extracted = False completion_mode = XBlockCompletionMode.EXCLUDED discussion_id = String(scope=Scope.settings, default=UNIQUE_ID) @@ -275,3 +277,10 @@ def _apply_metadata_and_policy(cls, block, node, runtime): for field_name, value in metadata.items(): if field_name in block.fields: setattr(block, field_name, value) + + +DiscussionXBlock = ( + _ExtractedDiscussionXBlock if USE_EXTRACTED_DISCUSSION_BLOCK.is_enabled() + else _BuiltInDiscussionXBlock +) + diff --git a/xmodule/html_block.py b/xmodule/html_block.py index 62949647cee3..93c6c14661fa 100644 --- a/xmodule/html_block.py +++ b/xmodule/html_block.py @@ -15,6 +15,7 @@ from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, List, Scope, String +from xblocks_contrib.html import HtmlBlock as _ExtractedHtmlBlock from common.djangoapps.xblock_django.constants import ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID from xmodule.contentstore.content import StaticContent @@ -22,8 +23,9 @@ from xmodule.edxnotes_utils import edxnotes from xmodule.html_checker import check_html from xmodule.stringify import stringify_children -from xmodule.util.misc import escape_html_characters +from xmodule.toggles import USE_EXTRACTED_HTML_BLOCK from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment +from xmodule.util.misc import escape_html_characters from xmodule.x_module import ( ResourceTemplates, shim_xmodule_js, @@ -50,6 +52,8 @@ class HtmlBlockMixin( # lint-amnesty, pylint: disable=abstract-method The HTML XBlock mixin. This provides the base class for all Html-ish blocks (including the HTML XBlock). """ + is_extracted = False + display_name = String( display_name=_("Display Name"), help=_("The display name for this component."), @@ -353,7 +357,7 @@ def index_dictionary(self): @edxnotes -class HtmlBlock(HtmlBlockMixin): # lint-amnesty, pylint: disable=abstract-method +class _BuiltInHtmlBlock(HtmlBlockMixin): # lint-amnesty, pylint: disable=abstract-method """ This is the actual HTML XBlock. Nothing extra is required; this is just a wrapper to include edxnotes support. @@ -489,3 +493,9 @@ def safe_parse_date(date): return datetime.strptime(date, '%B %d, %Y') except ValueError: # occurs for ill-formatted date values return datetime.today() + + +HtmlBlock = ( + _ExtractedHtmlBlock if USE_EXTRACTED_HTML_BLOCK.is_enabled() + else _BuiltInHtmlBlock +) diff --git a/xmodule/lti_block.py b/xmodule/lti_block.py index 55f940b381c2..91b8764effea 100644 --- a/xmodule/lti_block.py +++ b/xmodule/lti_block.py @@ -59,9 +59,9 @@ import hashlib import logging import textwrap -from xml.sax.saxutils import escape from unittest import mock from urllib import parse +from xml.sax.saxutils import escape import nh3 import oauthlib.oauth1 @@ -69,30 +69,30 @@ from lxml import etree from oauthlib.oauth1.rfc5849 import signature from pytz import UTC -from webob import Response from web_fragments.fragment import Fragment +from webob import Response from xblock.core import List, Scope, String, XBlock from xblock.fields import Boolean, Float -from xmodule.mako_block import MakoTemplateBlockBase - -from openedx.core.djangolib.markup import HTML, Text -from xmodule.editing_block import EditingMixin +from xblocks_contrib.lti import LTIBlock as _ExtractedLTIBlock from common.djangoapps.xblock_django.constants import ( ATTR_KEY_ANONYMOUS_USER_ID, ATTR_KEY_USER_ROLE, ) +from openedx.core.djangolib.markup import HTML, Text +from xmodule.editing_block import EditingMixin from xmodule.lti_2_util import LTI20BlockMixin, LTIError +from xmodule.mako_block import MakoTemplateBlockBase from xmodule.raw_block import EmptyDataRawMixin +from xmodule.toggles import USE_EXTRACTED_LTI_BLOCK from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment -from xmodule.xml_block import XmlMixin from xmodule.x_module import ( ResourceTemplates, shim_xmodule_js, XModuleMixin, XModuleToXBlockMixin, ) - +from xmodule.xml_block import XmlMixin log = logging.getLogger(__name__) @@ -274,7 +274,7 @@ class LTIFields: @XBlock.needs("mako") @XBlock.needs("user") @XBlock.needs("rebind_user") -class LTIBlock( +class _BuiltInLTIBlock( LTIFields, LTI20BlockMixin, EmptyDataRawMixin, @@ -366,6 +366,7 @@ class LTIBlock( Otherwise error message from LTI provider is generated. """ + is_extracted = False resources_dir = None uses_xmodule_styles_setup = True @@ -984,3 +985,9 @@ def is_past_due(self): else: close_date = due_date return close_date is not None and datetime.datetime.now(UTC) > close_date + + +LTIBlock = ( + _ExtractedLTIBlock if USE_EXTRACTED_LTI_BLOCK.is_enabled() + else _BuiltInLTIBlock +) diff --git a/xmodule/poll_block.py b/xmodule/poll_block.py index a1c9686f42ac..d4060910301d 100644 --- a/xmodule/poll_block.py +++ b/xmodule/poll_block.py @@ -13,14 +13,16 @@ from collections import OrderedDict from copy import deepcopy -from web_fragments.fragment import Fragment - from lxml import etree +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, Dict, List, Scope, String # lint-amnesty, pylint: disable=wrong-import-order +from xblocks_contrib.poll import PollBlock as _ExtractedPollBlock + from openedx.core.djangolib.markup import Text, HTML from xmodule.mako_block import MakoTemplateBlockBase from xmodule.stringify import stringify_children +from xmodule.toggles import USE_EXTRACTED_POLL_BLOCK from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment from xmodule.x_module import ( ResourceTemplates, @@ -30,13 +32,12 @@ ) from xmodule.xml_block import XmlMixin - log = logging.getLogger(__name__) _ = lambda text: text @XBlock.needs('mako') -class PollBlock( +class _BuiltInPollBlock( MakoTemplateBlockBase, XmlMixin, XModuleToXBlockMixin, @@ -44,6 +45,9 @@ class PollBlock( XModuleMixin, ): # pylint: disable=abstract-method """Poll Block""" + + is_extracted = False + # Name of poll to use in links to this poll display_name = String( help=_("The display name for this component."), @@ -244,3 +248,8 @@ def add_child(xml_obj, answer): # lint-amnesty, pylint: disable=unused-argument add_child(xml_object, answer) return xml_object + +PollBlock = ( + _ExtractedPollBlock if USE_EXTRACTED_POLL_BLOCK.is_enabled() + else _BuiltInPollBlock +) diff --git a/xmodule/toggles.py b/xmodule/toggles.py new file mode 100644 index 000000000000..7fc1fc774bfe --- /dev/null +++ b/xmodule/toggles.py @@ -0,0 +1,23 @@ +""" +Add Waffle flags to roll out the extracted XBlocks. +Flags will use to toggle between the old and new block quickly +without putting course content or user state at risk. + +Ticket: https://github.com/openedx/edx-platform/issues/35308 +""" +from edx_toggles.toggles import WaffleFlag + +# .. toggle_name: USE_EXTRACTED_WORD_CLOUD_BLOCK +# .. toggle_description: Enables the use of the extracted Word Cloud XBlock, which has been shifted to the 'openedx/xblocks-contrib' repo. +# .. toggle_warning: Not production-ready until https://github.com/openedx/edx-platform/issues/34840 is done. +# .. toggle_use_cases: temporary +# .. toggle_default: False +USE_EXTRACTED_WORD_CLOUD_BLOCK = WaffleFlag('xmodule.use_extracted_block.word_cloud', __name__) +# TODO: Add flag docs once above approved +USE_EXTRACTED_ANNOTATABLE_BLOCK = WaffleFlag('xmodule.use_extracted_block.annotatable', __name__) +USE_EXTRACTED_POLL_BLOCK = WaffleFlag('xmodule.use_extracted_block.poll', __name__) +USE_EXTRACTED_LTI_BLOCK = WaffleFlag('xmodule.use_extracted_block.lti', __name__) +USE_EXTRACTED_HTML_BLOCK = WaffleFlag('xmodule.use_extracted_block.html', __name__) +USE_EXTRACTED_DISCUSSION_BLOCK = WaffleFlag('xmodule.use_extracted_block.discussion', __name__) +USE_EXTRACTED_PROBLEM_BLOCK = WaffleFlag('xmodule.use_extracted_block.problem', __name__) +USE_EXTRACTED_VIDEO_BLOCK = WaffleFlag('xmodule.use_extracted_block.video', __name__) diff --git a/xmodule/video_block/video_block.py b/xmodule/video_block/video_block.py index ea9d1a44280a..683945823d0b 100644 --- a/xmodule/video_block/video_block.py +++ b/xmodule/video_block/video_block.py @@ -29,6 +29,7 @@ from xblock.core import XBlock from xblock.fields import ScopeIds from xblock.runtime import KvsFieldData +from xblocks_contrib.video import VideoBlock as _ExtractedVideoBlock from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE, ATTR_KEY_USER_ID from openedx.core.djangoapps.video_config.models import HLSPlaybackEnabledFlag, CourseYoutubeBlockedFlag @@ -47,8 +48,8 @@ from xmodule.mako_block import MakoTemplateBlockBase from xmodule.modulestore.inheritance import InheritanceKeyValueStore, own_metadata from xmodule.raw_block import EmptyDataRawMixin +from xmodule.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_fragment from xmodule.validation import StudioValidation, StudioValidationMessage -from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment from xmodule.video_block import manage_video_subtitles_save from xmodule.x_module import ( PUBLIC_VIEW, STUDENT_VIEW, @@ -56,7 +57,6 @@ XModuleMixin, XModuleToXBlockMixin, ) from xmodule.xml_block import XmlMixin, deserialize_field, is_pointer_tag, name_to_pathname - from .bumper_utils import bumperize from .sharing_sites import sharing_sites_info_for_video from .transcripts_utils import ( @@ -70,6 +70,7 @@ from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers from .video_utils import create_youtube_string, format_xml_exception_message, get_poster, rewrite_video_url from .video_xfields import VideoFields +from ..toggles import USE_EXTRACTED_VIDEO_BLOCK # The following import/except block for edxval is temporary measure until # edxval is a proper XBlock Runtime Service. @@ -119,7 +120,7 @@ @XBlock.wants('settings', 'completion', 'i18n', 'request_cache') @XBlock.needs('mako', 'user') -class VideoBlock( +class _BuiltInVideoBlock( VideoFields, VideoTranscriptsMixin, VideoStudioViewHandlers, VideoStudentViewHandlers, EmptyDataRawMixin, XmlMixin, EditingMixin, XModuleToXBlockMixin, ResourceTemplates, XModuleMixin, LicenseMixin): @@ -134,6 +135,7 @@ class VideoBlock( """ + is_extracted = False has_custom_completion = True completion_mode = XBlockCompletionMode.COMPLETABLE @@ -1260,3 +1262,9 @@ def _poster(self): edx_video_id=self.edx_video_id.strip() ) return None + + +VideoBlock = ( + _ExtractedVideoBlock if USE_EXTRACTED_VIDEO_BLOCK.is_enabled() + else _BuiltInVideoBlock +) diff --git a/xmodule/word_cloud_block.py b/xmodule/word_cloud_block.py index d678f2a9a9f5..18249d453f43 100644 --- a/xmodule/word_cloud_block.py +++ b/xmodule/word_cloud_block.py @@ -15,6 +15,7 @@ from xblock.fields import Boolean, Dict, Integer, List, Scope, String from xmodule.editing_block import EditingMixin from xmodule.raw_block import EmptyDataRawMixin +from xmodule.toggles import USE_EXTRACTED_WORD_CLOUD_BLOCK from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment from xmodule.xml_block import XmlMixin from xmodule.x_module import ( @@ -23,6 +24,7 @@ XModuleMixin, XModuleToXBlockMixin, ) +from xblocks_contrib.word_cloud import WordCloudBlock as _ExtractedWordCloudBlock log = logging.getLogger(__name__) # Make '_' a no-op so we can scrape strings. Using lambda instead of @@ -41,7 +43,7 @@ def pretty_bool(value): @XBlock.needs('mako') -class WordCloudBlock( # pylint: disable=abstract-method +class _BuiltInWordCloudBlock( # pylint: disable=abstract-method EmptyDataRawMixin, XmlMixin, EditingMixin, @@ -53,6 +55,8 @@ class WordCloudBlock( # pylint: disable=abstract-method Word Cloud XBlock. """ + is_extracted = False + display_name = String( display_name=_("Display Name"), help=_("The display name for this component."), @@ -308,3 +312,9 @@ def index_dictionary(self): xblock_body["content_type"] = "Word Cloud" return xblock_body + + +WordCloudBlock = ( + _ExtractedWordCloudBlock if USE_EXTRACTED_WORD_CLOUD_BLOCK.is_enabled() + else _BuiltInWordCloudBlock +)