diff --git a/lms/templates/annotatable.html b/lms/templates/annotatable.html deleted file mode 100644 index dcd51a0cd52e..000000000000 --- a/lms/templates/annotatable.html +++ /dev/null @@ -1,31 +0,0 @@ -<%! from django.utils.translation import gettext as _ %> - -
-
- % if display_name is not UNDEFINED and display_name is not None: -

${display_name | h}

- % endif -
- - % if instructions_html is not UNDEFINED and instructions_html is not None: -
-
- ${_("Instructions")} - ${_("Collapse Instructions")} -
-
- ${instructions_html} -
-
- % endif - -
-
- ${_("Guided Discussion")} - ${_("Hide Annotations")} -
-
- ${content_html} -
-
-
diff --git a/openedx/core/lib/xblock_serializer/utils.py b/openedx/core/lib/xblock_serializer/utils.py index 89d60926691c..04654c473e23 100644 --- a/openedx/core/lib/xblock_serializer/utils.py +++ b/openedx/core/lib/xblock_serializer/utils.py @@ -195,7 +195,7 @@ def override_export_fs(block): in that it relies on `XmlMixin.export_to_file` (or `CustomTagBlock.export_to_file`) method to control whether a block has to be exported as two files (one .olx pointing to one .xml) file, or a single XML node. - For the legacy blocks (`AnnotatableBlock` for instance) `export_to_file` returns `True` by default. + For most legacy blocks, `export_to_file` returns `True` by default. The only exception is `CustomTagBlock`, for which this method was originally developed, as customtags don't have to be exported as separate files. diff --git a/openedx/envs/common.py b/openedx/envs/common.py index 31b0015d34d9..f8fc759e3fb1 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -2068,14 +2068,6 @@ def add_optional_apps(optional_apps, installed_apps): # .. toggle_target_removal_date: 2026-04-10 USE_EXTRACTED_WORD_CLOUD_BLOCK = True -# .. toggle_name: USE_EXTRACTED_ANNOTATABLE_BLOCK -# .. toggle_default: True -# .. toggle_implementation: DjangoSetting -# .. toggle_description: Enables the use of the extracted annotatable XBlock, which has been shifted to the 'openedx/xblocks-contrib' repo. -# .. toggle_use_cases: temporary -# .. toggle_creation_date: 2024-11-10 -# .. toggle_target_removal_date: 2026-04-10 -USE_EXTRACTED_ANNOTATABLE_BLOCK = True # .. toggle_name: USE_EXTRACTED_POLL_QUESTION_BLOCK # .. toggle_default: True diff --git a/pyproject.toml b/pyproject.toml index 44488938ec95..b2c80f496f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,8 +16,8 @@ xmodule = ["js/module/*"] [project.entry-points."xblock.v1"] about = "xmodule.html_block:AboutBlock" +annotatable = "xblocks_contrib:AnnotatableBlock" book = "xmodule.template_block:TranslateCustomTagBlock" -annotatable = "xmodule.annotatable_block:AnnotatableBlock" chapter = "xmodule.seq_block:SectionBlock" conditional = "xmodule.conditional_block:ConditionalBlock" course = "xmodule.course_block:CourseBlock" diff --git a/webpack.builtinblocks.config.js b/webpack.builtinblocks.config.js index 20d8790fc5b5..0bec8427c87b 100644 --- a/webpack.builtinblocks.config.js +++ b/webpack.builtinblocks.config.js @@ -1,16 +1,5 @@ module.exports = { entry: { - AnnotatableBlockDisplay: [ - './xmodule/js/src/xmodule.js', - './xmodule/js/src/html/display.js', - './xmodule/js/src/annotatable/display.js', - './xmodule/js/src/javascript_loader.js', - './xmodule/js/src/collapsible.js' - ], - AnnotatableBlockEditor: [ - './xmodule/js/src/xmodule.js', - './xmodule/js/src/raw/edit/xml.js' - ], ConditionalBlockDisplay: [ './xmodule/js/src/xmodule.js', './xmodule/js/src/conditional/display.js', diff --git a/xmodule/annotatable_block.py b/xmodule/annotatable_block.py deleted file mode 100644 index 2d2347d851a4..000000000000 --- a/xmodule/annotatable_block.py +++ /dev/null @@ -1,229 +0,0 @@ -# pylint: disable=missing-module-docstring - -import logging -import textwrap -import warnings - -from django.conf import settings -from lxml import etree -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.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_fragment -from xmodule.x_module import ResourceTemplates, XModuleMixin, XModuleToXBlockMixin, shim_xmodule_js -from xmodule.xml_block import XmlMixin - -log = logging.getLogger(__name__) - -# Make '_' a no-op so we can scrape strings. Using lambda instead of -# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file -_ = lambda text: text - - -@XBlock.needs('mako') -class _BuiltInAnnotatableBlock( - RawMixin, - XmlMixin, - EditingMixin, - XModuleToXBlockMixin, - ResourceTemplates, - XModuleMixin, -): - """ - Annotatable XBlock. - - .. deprecated:: 2026-03 - This built-in annotatable block is deprecated. Please use the extracted ``AnnotatableBlock`` - from ``xblocks_contrib.annotatable`` instead. - """ - - is_extracted = False - - data = String( - help=_("XML data for the annotation"), - scope=Scope.content, - default=textwrap.dedent(HTML(""" - - -

Enter your (optional) instructions for the exercise in HTML format.

-

Annotations are specified by an {}annotation{} tag which may may have the following attributes:

- -
-

Add your HTML with annotation spans here.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sodales laoreet est, egestas gravida felis egestas nec. Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.

-

Nulla facilisi. Pellentesque id vestibulum libero. Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.

-
- """).format(Text('<'), Text('>'))) - ) - display_name = String( - display_name=_("Display Name"), - help=_("The display name for this component."), - scope=Scope.settings, - default=_('Annotation'), - ) - - uses_xmodule_styles_setup = True - - studio_js_module_name = "XMLEditingDescriptor" - mako_template = "widgets/raw-edit.html" - - icon_class = 'annotatable' - resources_dir = None - - HIGHLIGHT_COLORS = ['yellow', 'orange', 'purple', 'blue', 'green'] - - def _get_annotation_class_attr(self, index, el): # pylint: disable=unused-argument - """ Returns a dict with the CSS class attribute to set on the annotation - and an XML key to delete from the element. - """ - - attr = {} - cls = ['annotatable-span', 'highlight'] - highlight_key = 'highlight' - color = el.get(highlight_key) - - if color is not None: - if color in self.HIGHLIGHT_COLORS: - cls.append('highlight-' + color) - attr['_delete'] = highlight_key - attr['value'] = ' '.join(cls) - - return {'class': attr} - - def _get_annotation_data_attr(self, index, el): # pylint: disable=unused-argument - """ Returns a dict in which the keys are the HTML data attributes - to set on the annotation element. Each data attribute has a - corresponding 'value' and (optional) '_delete' key to specify - an XML attribute to delete. - """ - - data_attrs = {} - attrs_map = { - 'body': 'data-comment-body', - 'title': 'data-comment-title', - 'problem': 'data-problem-id' - } - - for xml_key in attrs_map.keys(): # pylint: disable=consider-iterating-dictionary - if xml_key in el.attrib: - value = el.get(xml_key, '') - html_key = attrs_map[xml_key] - data_attrs[html_key] = {'value': value, '_delete': xml_key} - - return data_attrs - - def _render_annotation(self, index, el): - """ Renders an annotation element for HTML output. """ - attr = {} - attr.update(self._get_annotation_class_attr(index, el)) - attr.update(self._get_annotation_data_attr(index, el)) - - el.tag = 'span' - - for key in attr.keys(): # pylint: disable=consider-iterating-dictionary - el.set(key, attr[key]['value']) - if '_delete' in attr[key] and attr[key]['_delete'] is not None: - delete_key = attr[key]['_delete'] - del el.attrib[delete_key] - - def _render_content(self): - """ Renders annotatable content with annotation spans and returns HTML. """ - - xmltree = etree.fromstring(self.data) - self._extract_instructions(xmltree) - - xmltree.tag = 'div' - if 'display_name' in xmltree.attrib: - del xmltree.attrib['display_name'] - - index = 0 - for el in xmltree.findall('.//annotation'): - self._render_annotation(index, el) - index += 1 - - return etree.tostring(xmltree, encoding='unicode') - - def _extract_instructions(self, xmltree): - """ Removes from the xmltree and returns them as a string, otherwise None. """ - instructions = xmltree.find('instructions') - if instructions is not None: - instructions.tag = 'div' - xmltree.remove(instructions) - return etree.tostring(instructions, encoding='unicode') - return None - - def get_html(self): - """ Renders parameters to template. """ - - xmltree = etree.fromstring(self.data) - instructions = self._extract_instructions(xmltree) - - context = { - 'display_name': self.display_name_with_default, - 'element_id': self.location.html_id(), - 'instructions_html': instructions, - 'content_html': self._render_content() - } - - return self.runtime.service(self, 'mako').render_lms_template('annotatable.html', context) - - def student_view(self, context): # pylint: disable=unused-argument - """ - Renders the output that a student will see. - """ - fragment = Fragment() - fragment.add_content(self.get_html()) - add_css_to_fragment(fragment, 'AnnotatableBlockDisplay.css') - add_webpack_js_to_fragment(fragment, 'AnnotatableBlockDisplay') - shim_xmodule_js(fragment, 'Annotatable') - - return fragment - - def studio_view(self, _context): - """ - Return the studio view. - """ - fragment = Fragment( - self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context()) - ) - add_css_to_fragment(fragment, 'AnnotatableBlockEditor.css') - add_webpack_js_to_fragment(fragment, 'AnnotatableBlockEditor') - shim_xmodule_js(fragment, self.studio_js_module_name) - return fragment - - -AnnotatableBlock = None - - -def reset_class(): - """Reset class as per django settings flag""" - global AnnotatableBlock - AnnotatableBlock = ( - _ExtractedAnnotatableBlock if settings.USE_EXTRACTED_ANNOTATABLE_BLOCK else _BuiltInAnnotatableBlock - ) - return AnnotatableBlock - - -reset_class() -AnnotatableBlock.__name__ = "AnnotatableBlock" - -if not settings.USE_EXTRACTED_ANNOTATABLE_BLOCK: - warnings.warn( - "The built-in `xmodule.annotatable_block` AnnotatableBlock implementation is deprecated. " - "To fix this warning, enable `USE_EXTRACTED_ANNOTATABLE_BLOCK` (set it to True) to use " - "`xblocks_contrib.annotatable.AnnotatableBlock` instead. " - "Support for the built-in implementation, and the `USE_EXTRACTED_ANNOTATABLE_BLOCK` setting, " - "will be removed in Willow.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/xmodule/js/fixtures/annotatable.html b/xmodule/js/fixtures/annotatable.html deleted file mode 100644 index 5f80304a87db..000000000000 --- a/xmodule/js/fixtures/annotatable.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
-
-

First Annotation Exercise

-
-
-
- Instructions - Collapse Instructions -
-
-

The main goal of this exercise is to start practicing the art of slow reading.

-
-
-
-
- Guided Discussion - Hide Annotations -
-
-
- |87 No, those who are really responsible are Zeus and Fate [Moira] and the Fury [Erinys] who roams in the mist.
- |88 They are the ones who
- |100 He [= Zeus], making a formal declaration [eukhesthai], spoke up at a meeting of all the gods and said:
- |101 “hear me, all gods and all goddesses,
- |113 but he swore a great oath. - And right then and there
-
-
-
- -
Return to Annotation
-
Return to Annotation
-
Return to Annotation
diff --git a/xmodule/js/spec/annotatable/display_spec.js b/xmodule/js/spec/annotatable/display_spec.js deleted file mode 100644 index 5ad45862ba44..000000000000 --- a/xmodule/js/spec/annotatable/display_spec.js +++ /dev/null @@ -1,10 +0,0 @@ -describe('Annotatable', function() { - beforeEach(() => loadFixtures('annotatable.html')); - describe('constructor', function() { - const el = $('.xblock-student_view.xmodule_AnnotatableModule'); - beforeEach(function() { - this.annotatable = new Annotatable(el); - }); - it('works', () => expect(1).toBe(1)); - }); -}); diff --git a/xmodule/js/src/annotatable/display.js b/xmodule/js/src/annotatable/display.js deleted file mode 100644 index 5777b95313de..000000000000 --- a/xmodule/js/src/annotatable/display.js +++ /dev/null @@ -1,443 +0,0 @@ -// Once generated by CoffeeScript 1.9.3, but now lives as pure JS -/* eslint-disable */ -// TODO: Examine all of the xss-lint exceptions (https://openedx.atlassian.net/browse/PLAT-2084) -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.Annotatable = (function() { - Annotatable.prototype._debug = false; - - - /* - selectors for the annotatable xmodule - */ - - Annotatable.prototype.wrapperSelector = '.annotatable-wrapper'; - - Annotatable.prototype.toggleAnnotationsSelector = '.annotatable-toggle-annotations'; - - Annotatable.prototype.toggleInstructionsSelector = '.annotatable-toggle-instructions'; - - Annotatable.prototype.instructionsSelector = '.annotatable-instructions'; - - Annotatable.prototype.sectionSelector = '.annotatable-section'; - - Annotatable.prototype.spanSelector = '.annotatable-span'; - - Annotatable.prototype.replySelector = '.annotatable-reply'; - - - /* - these selectors are for responding to events from the annotation capa problem type - */ - - Annotatable.prototype.problemXModuleSelector = '.xmodule_CapaModule'; - - Annotatable.prototype.problemSelector = 'div.problem'; - - Annotatable.prototype.problemInputSelector = 'div.problem .annotation-input'; - - Annotatable.prototype.problemReturnSelector = 'div.problem .annotation-return'; - - function Annotatable(el) { - this.onMoveTip = bind(this.onMoveTip, this); - this.onShowTip = bind(this.onShowTip, this); - this.onClickReturn = bind(this.onClickReturn, this); - this.onClickReply = bind(this.onClickReply, this); - this.onClickToggleInstructions = bind(this.onClickToggleInstructions, this); - this.onClickToggleAnnotations = bind(this.onClickToggleAnnotations, this); - if (this._debug) { - console.log('loaded Annotatable'); - } - this.el = el; - this.$el = $(el); - this.init(); - } - - Annotatable.prototype.$ = function(selector) { - return $(selector, this.el); - }; - - Annotatable.prototype.init = function() { - this.initEvents(); - return this.initTips(); - }; - - Annotatable.prototype.initEvents = function() { - - /* - Initialize toggle handlers for the instructions and annotations sections - */ - var ref; - ref = [false, false], this.annotationsHidden = ref[0], this.instructionsHidden = ref[1]; - this.$(this.toggleAnnotationsSelector).bind('click', this.onClickToggleAnnotations); - this.$(this.toggleInstructionsSelector).bind('click', this.onClickToggleInstructions); - - /* - Initialize handler for 'reply to annotation' events that scroll to - the associated problem. The reply buttons are part of the tooltip - content. It's important that the tooltips be configured to render - as descendants of the annotation module and *not* the document.body. - */ - this.$el.on('click', this.replySelector, this.onClickReply); - - /* - Initialize handler for 'return to annotation' events triggered from problems. - 1) There are annotationinput capa problems rendered on the page - 2) Each one has an embedded return link (see annotation capa problem template). - Since the capa problem injects HTML content via AJAX, the best we can do is - is let the click events bubble up to the body and handle them there. - */ - return $(document).on('click', this.problemReturnSelector, this.onClickReturn); - }; - - Annotatable.prototype.initTips = function() { - - /* - tooltips are used to display annotations for highlighted text spans - */ - return this.$(this.spanSelector).each((function(_this) { - return function(index, el) { - return $(el).qtip(_this.getSpanTipOptions(el)); - }; - })(this)); - }; - - Annotatable.prototype.getSpanTipOptions = function(el) { - return { - content: { - title: { - text: this.makeTipTitle(el) - }, - text: this.makeTipContent(el) - }, - position: { - - /* - of tooltip - */ - my: 'bottom center', - - /* - of target - */ - at: 'top center', - - /* - where the tooltip was triggered (i.e. the annotation span) - */ - target: $(el), - container: this.$(this.wrapperSelector), - adjust: { - y: -5 - } - }, - show: { - event: 'click mouseenter', - solo: true - }, - hide: { - event: 'click mouseleave', - delay: 500, - - /* - don't hide the tooltip if it is moused over - */ - fixed: true - }, - style: { - classes: 'ui-tooltip-annotatable' - }, - events: { - show: this.onShowTip, - move: this.onMoveTip - } - }; - }; - - Annotatable.prototype.onClickToggleAnnotations = function(e) { - return this.toggleAnnotations(); - }; - - Annotatable.prototype.onClickToggleInstructions = function(e) { - return this.toggleInstructions(); - }; - - Annotatable.prototype.onClickReply = function(e) { - return this.replyTo(e.currentTarget); - }; - - Annotatable.prototype.onClickReturn = function(e) { - return this.returnFrom(e.currentTarget); - }; - - Annotatable.prototype.onShowTip = function(event, api) { - if (this.annotationsHidden) { - return event.preventDefault(); - } - }; - - Annotatable.prototype.onMoveTip = function(event, api, position) { - - /* - This method handles a vertical positioning bug in Firefox as - well as an edge case in which a tooltip is displayed above a - non-overlapping span like this: - - (( TOOLTIP )) - \/ - text text text ... text text text ...... - - - The problem is that the tooltip looks disconnected from both spans, so - we should re-position the tooltip to appear above the span. - */ - var adjust_y, container, container_offset, focus_rect, is_non_overlapping, offset_left, offset_top, rect_center, rect_top, rects, ref, ref1, ref2, target, tip, tip_height, tip_left, tip_top, tip_width, win_width; - tip = api.elements.tooltip; - adjust_y = ((ref = api.options.position) != null ? (ref1 = ref.adjust) != null ? ref1.y : void 0 : void 0) || 0; - container = ((ref2 = api.options.position) != null ? ref2.container : void 0) || $('body'); - target = api.elements.target; - rects = $(target).get(0).getClientRects(); - is_non_overlapping = (rects != null ? rects.length : void 0) === 2 && rects[0].left > rects[1].right; - if (is_non_overlapping) { - - /* - we want to choose the largest of the two non-overlapping spans and display - the tooltip above the center of it (see api.options.position settings) - */ - focus_rect = (rects[0].width > rects[1].width ? rects[0] : rects[1]); - } else { - - /* - always compute the new position because Firefox doesn't - properly vertically position the tooltip - */ - focus_rect = rects[0]; - } - rect_center = focus_rect.left + (focus_rect.width / 2); - rect_top = focus_rect.top; - tip_width = $(tip).width(); - tip_height = $(tip).height(); - - /* - tooltip is positioned relative to its container, so we need to factor in offsets - */ - container_offset = $(container).offset(); - offset_left = -container_offset.left; - offset_top = $(document).scrollTop() - container_offset.top; - tip_left = offset_left + rect_center - (tip_width / 2); - tip_top = offset_top + rect_top - tip_height + adjust_y; - - /* - make sure the new tip position doesn't clip the edges of the screen - */ - win_width = $(window).width(); - if (tip_left < offset_left) { - tip_left = offset_left; - } else if (tip_left + tip_width > win_width + offset_left) { - tip_left = win_width + offset_left - tip_width; - } - - /* - final step: update the position object (used by qtip2 to show the tip after the move event) - */ - return $.extend(position, { - 'left': tip_left, - 'top': tip_top - }); - }; - - Annotatable.prototype.getSpanForProblemReturn = function(el) { - var problem_id; - problem_id = $(this.problemReturnSelector).index(el); - return this.$(this.spanSelector).filter("[data-problem-id='" + problem_id + "']"); - }; - - Annotatable.prototype.getProblem = function(el) { - var problem_id; - problem_id = this.getProblemId(el); - return $(this.problemInputSelector).eq(problem_id); - }; - - Annotatable.prototype.getProblemId = function(el) { - return $(el).data('problem-id'); - }; - - Annotatable.prototype.toggleAnnotations = function() { - var hide; - hide = (this.annotationsHidden = !this.annotationsHidden); - this.toggleAnnotationButtonText(hide); - this.toggleSpans(hide); - return this.toggleTips(hide); - }; - - Annotatable.prototype.toggleTips = function(hide) { - var visible; - visible = this.findVisibleTips(); - return this.hideTips(visible); - }; - - Annotatable.prototype.toggleAnnotationButtonText = function(hide) { - var buttonText; - if (hide) { - buttonText = gettext('Show Annotations'); - } else { - buttonText = gettext('Hide Annotations'); - } - return this.$(this.toggleAnnotationsSelector).text(buttonText); - }; - - Annotatable.prototype.toggleInstructions = function() { - var hide; - hide = (this.instructionsHidden = !this.instructionsHidden); - this.toggleInstructionsButton(hide); - return this.toggleInstructionsText(hide); - }; - - Annotatable.prototype.toggleInstructionsButton = function(hide) { - var cls, txt; - if (hide) { - txt = gettext('Expand Instructions'); - } else { - txt = gettext('Collapse Instructions'); - } - cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']); - return this.$(this.toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1]); - }; - - Annotatable.prototype.toggleInstructionsText = function(hide) { - var slideMethod; - slideMethod = (hide ? 'slideUp' : 'slideDown'); - return this.$(this.instructionsSelector)[slideMethod](); - }; - - Annotatable.prototype.toggleSpans = function(hide) { - return this.$(this.spanSelector).toggleClass('hide', hide, 250); - }; - - Annotatable.prototype.replyTo = function(buttonEl) { - var el, offset; - offset = -20; - el = this.getProblem(buttonEl); - if (el.length > 0) { - return this.scrollTo(el, this.afterScrollToProblem, offset); - } else { - if (this._debug) { - return console.log('problem not found. event: ', e); - } - } - }; - - Annotatable.prototype.returnFrom = function(buttonEl) { - var el, offset; - offset = -200; - el = this.getSpanForProblemReturn(buttonEl); - if (el.length > 0) { - return this.scrollTo(el, this.afterScrollToSpan, offset); - } else { - if (this._debug) { - return console.log('span not found. event:', e); - } - } - }; - - Annotatable.prototype.scrollTo = function(el, after, offset) { - if (offset == null) { - offset = -20; - } - if ($(el).length > 0) { - return $('html,body').scrollTo(el, { - duration: 500, - onAfter: this._once((function(_this) { - return function() { - return after != null ? after.call(_this, el) : void 0; - }; - })(this)), - offset: offset - }); - } - }; - - Annotatable.prototype.afterScrollToProblem = function(problem_el) { - return problem_el.effect('highlight', {}, 500); - }; - - Annotatable.prototype.afterScrollToSpan = function(span_el) { - return span_el.addClass('selected', 400, 'swing', function() { - return span_el.removeClass('selected', 400, 'swing'); - }); - }; - - Annotatable.prototype.makeTipContent = function(el) { - return (function(_this) { - return function(api) { - var comment, problem_id, reply, text; - text = $(el).data('comment-body'); - comment = _this.createComment(text); - problem_id = _this.getProblemId(el); - reply = _this.createReplyLink(problem_id); - return $(comment).add(reply); - }; - })(this); - }; - - Annotatable.prototype.makeTipTitle = function(el) { - return (function(_this) { - return function(api) { - var title; - title = $(el).data('comment-title'); - if (title) { - return title; - } else { - return gettext('Commentary'); - } - }; - })(this); - }; - - Annotatable.prototype.createComment = function(text) { - return $("
" + text + "
"); // xss-lint: disable=javascript-concat-html - }; - - Annotatable.prototype.createReplyLink = function(problem_id) { - var linktxt; - linktxt = gettext('Reply to Annotation'); - return $("" + linktxt + ""); // xss-lint: disable=javascript-concat-html - }; - - Annotatable.prototype.findVisibleTips = function() { - var visible; - visible = []; - this.$(this.spanSelector).each(function(index, el) { - var api, tip; - api = $(el).qtip('api'); - tip = $(api != null ? api.elements.tooltip : void 0); - if (tip.is(':visible')) { - return visible.push(el); - } - }); - return visible; - }; - - Annotatable.prototype.hideTips = function(elements) { - return $(elements).qtip('hide'); - }; - - Annotatable.prototype._once = function(fn) { - var done; - done = false; - return (function(_this) { - return function() { - if (!done) { - fn.call(_this); - } - return done = true; - }; - })(this); - }; - - return Annotatable; - - })(); - -}).call(this); diff --git a/xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css b/xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css deleted file mode 100644 index 0a17543285ce..000000000000 --- a/xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css +++ /dev/null @@ -1,232 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700"); - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-wrapper { - position: relative; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-header { - margin-bottom: 0.5em; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section { - position: relative; - padding: 0.5em 1em; - border: 1px solid var(--gray-l3, #c8c8c8); - border-radius: 0.5em; - margin-bottom: 0.5em; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section.shaded { - background-color: #ededed; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-title { - font-weight: bold; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-title a { - font-weight: normal; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-body { - border-top: 1px solid var(--gray-l3, #c8c8c8); - margin-top: 0.5em; - padding-top: 0.5em; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-body:after { - content: ""; - display: table; - clear: both; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template { - list-style: disc; - margin-left: 4em; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template b { - font-weight: bold; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template i { - font-style: italic; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template code { - display: inline; - white-space: pre; - font-family: Courier New, monospace; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle { - position: absolute; - right: 0; - margin: 2px 1em 2px 0; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle.expanded::after { - content: " \2191"; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle.collapsed::after { - content: " \2193"; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span { - display: inline; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight { - background-color: rgba(255, 255, 10, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight.selected { - background-color: rgba(255, 255, 10, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-yellow { - background-color: rgba(255, 255, 10, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-yellow.selected { - background-color: rgba(255, 255, 10, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-red { - background-color: rgba(178, 19, 16, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-red.selected { - background-color: rgba(178, 19, 16, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-orange { - background-color: rgba(255, 165, 0, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-orange.selected { - background-color: rgba(255, 165, 0, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-green { - background-color: rgba(25, 255, 132, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-green.selected { - background-color: rgba(25, 255, 132, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-blue { - background-color: rgba(35, 163, 255, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-blue.selected { - background-color: rgba(35, 163, 255, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-purple { - background-color: rgba(115, 9, 178, 0.3); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-purple.selected { - background-color: rgba(115, 9, 178, 0.9); -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.hide { - cursor: none; - background-color: inherit; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.hide .annotatable-icon { - display: none; -} - -.xmodule_display.xmodule_AnnotatableBlock .annotatable-span .annotatable-comment { - display: none; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip { - font-size: 0.875em; - border: 1px solid #333; - border-radius: 1em; - background-color: rgba(0, 0, 0, 0.85); - color: var(--white, #fff); - -webkit-font-smoothing: antialiased; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar { - font-size: 1em; - color: inherit; - background-color: transparent; - padding: calc((var(--baseline, 20px) / 4)) calc((var(--baseline, 20px) / 2)); - border: none; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-title { - padding: calc((var(--baseline, 20px) / 4)) 0; - border-bottom: 2px solid #333; - font-weight: bold; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-icon { - right: 10px; - background: #333; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-state-hover { - color: inherit; - border: 1px solid var(--gray-l3, #c8c8c8); -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-content { - color: inherit; - font-size: 0.875em; - text-align: left; - font-weight: 400; - padding: 0 calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 2)); - background-color: transparent; - border-color: transparent; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip p { - color: inherit; - line-height: normal; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable { - max-width: 375px; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content { - padding: 0 calc((var(--baseline, 20px) / 2)); -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-comment { - display: block; - margin: 0 0 calc((var(--baseline, 20px) / 2)) 0; - max-height: 225px; - overflow: auto; - line-height: normal; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-reply { - display: block; - border-top: 2px solid #333; - padding: calc((var(--baseline, 20px) / 4)) 0; - margin: 0; - text-align: center; -} - -.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable::after { - content: ''; - display: inline-block; - position: absolute; - bottom: -20px; - left: 50%; - height: 0; - width: 0; - margin-left: calc(-1 * (var(--baseline, 20px) / 4)); - border: 10px solid transparent; - border-top-color: rgba(0, 0, 0, 0.85); -} diff --git a/xmodule/static/css-builtin-blocks/AnnotatableBlockEditor.css b/xmodule/static/css-builtin-blocks/AnnotatableBlockEditor.css deleted file mode 100644 index 498cbda9ffc4..000000000000 --- a/xmodule/static/css-builtin-blocks/AnnotatableBlockEditor.css +++ /dev/null @@ -1,5 +0,0 @@ -.xmodule_edit.xmodule_AnnotatableBlock .CodeMirror { - background: #fff; - font-size: 13px; - color: #3c3c3c; -} diff --git a/xmodule/tests/test_annotatable_block.py b/xmodule/tests/test_annotatable_block.py deleted file mode 100644 index d1ea0087b4b1..000000000000 --- a/xmodule/tests/test_annotatable_block.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Annotatable block tests""" - - -from django.test import TestCase -from django.test.utils import override_settings -from lxml import etree -from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator -from xblock.field_data import DictFieldData -from xblock.fields import ScopeIds - -from xmodule import annotatable_block - -from . import get_test_system - - -class _AnnotatableBlockTestCaseBase(TestCase): # pylint: disable=missing-class-docstring - sample_xml = ''' - - Read the text. -

- Sing, - O goddess, - the anger of Achilles son of Peleus, - that brought countless ills upon the Achaeans. Many a brave soul did it send - hurrying down to Hades, and many a hero did it yield a prey to dogs and -

vultures, for so were the counsels - of Jove fulfilled from the day on which the son of Atreus, king of men, and great - Achilles, first fell out with one another.
-

- The Iliad of Homer by Samuel Butler -
- ''' - - __test__ = False - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.annotatable_class = annotatable_block.reset_class() - - def setUp(self): - super().setUp() - self.annotatable = self.annotatable_class( - get_test_system(), - DictFieldData({'data': self.sample_xml}), - ScopeIds(None, None, None, BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'category', 'name')) - ) - - def test_annotation_data_attr(self): - el = etree.fromstring('test') - - expected_attr = { - 'data-comment-body': {'value': 'foo', '_delete': 'body'}, - 'data-comment-title': {'value': 'bar', '_delete': 'title'}, - 'data-problem-id': {'value': '0', '_delete': 'problem'} - } - - actual_attr = self.annotatable._get_annotation_data_attr(0, el) # pylint: disable=protected-access - - assert isinstance(actual_attr, dict) - self.assertDictEqual(expected_attr, actual_attr) # noqa: PT009 - - def test_annotation_class_attr_default(self): - xml = 'test' - el = etree.fromstring(xml) - - expected_attr = {'class': {'value': 'annotatable-span highlight'}} - actual_attr = self.annotatable._get_annotation_class_attr(0, el) # pylint: disable=protected-access - - assert isinstance(actual_attr, dict) - self.assertDictEqual(expected_attr, actual_attr) # noqa: PT009 - - def test_annotation_class_attr_with_valid_highlight(self): - xml = 'test' - - for color in self.annotatable.HIGHLIGHT_COLORS: - el = etree.fromstring(xml.format(highlight=color)) - value = f'annotatable-span highlight highlight-{color}' - - expected_attr = { - 'class': { - 'value': value, - '_delete': 'highlight' - } - } - actual_attr = self.annotatable._get_annotation_class_attr(0, el) # pylint: disable=protected-access - - assert isinstance(actual_attr, dict) - self.assertDictEqual(expected_attr, actual_attr) # noqa: PT009 - - def test_annotation_class_attr_with_invalid_highlight(self): - xml = 'test' - - for invalid_color in ['rainbow', 'blink', 'invisible', '', None]: - el = etree.fromstring(xml.format(highlight=invalid_color)) - expected_attr = { - 'class': { - 'value': 'annotatable-span highlight', - '_delete': 'highlight' - } - } - actual_attr = self.annotatable._get_annotation_class_attr(0, el) # pylint: disable=protected-access - - assert isinstance(actual_attr, dict) - self.assertDictEqual(expected_attr, actual_attr) # noqa: PT009 - - def test_render_annotation(self): - expected_html = 'z' # pylint: disable=line-too-long - expected_el = etree.fromstring(expected_html) - - actual_el = etree.fromstring('z') - self.annotatable._render_annotation(0, actual_el) # pylint: disable=protected-access - - assert expected_el.tag == actual_el.tag - assert expected_el.text == actual_el.text - self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib)) # noqa: PT009 - - def test_render_content(self): - content = self.annotatable._render_content() # pylint: disable=protected-access - el = etree.fromstring(content) - - assert 'div' == el.tag, 'root tag is a div' - - expected_num_annotations = 5 - actual_num_annotations = el.xpath('count(//span[contains(@class,"annotatable-span")])') - assert expected_num_annotations == actual_num_annotations, 'check number of annotations' - - def test_get_html(self): - context = self.annotatable.get_html() - for key in ['display_name', 'element_id', 'content_html', 'instructions_html']: - assert key in context - - def test_extract_instructions(self): - xmltree = etree.fromstring(self.sample_xml) - - expected_xml = "
Read the text.
" - actual_xml = self.annotatable._extract_instructions(xmltree) # pylint: disable=protected-access - assert actual_xml is not None - assert expected_xml.strip() == actual_xml.strip() - - xmltree = etree.fromstring('foo') - actual = self.annotatable._extract_instructions(xmltree) # pylint: disable=protected-access - assert actual is None - - def test_instruction_removal(self): - xmltree = etree.fromstring(self.sample_xml) - instructions = self.annotatable._extract_instructions(xmltree) # pylint: disable=protected-access - - assert instructions is not None - assert "Read the text." in instructions - assert xmltree.find("instructions") is None - - -@override_settings(USE_EXTRACTED_ANNOTATABLE_BLOCK=True) -class ExtractedAnnotatableBlockTestCase(_AnnotatableBlockTestCaseBase): - __test__ = True - - -@override_settings(USE_EXTRACTED_ANNOTATABLE_BLOCK=False) -class BuiltInAnnotatableBlockTestCase(_AnnotatableBlockTestCaseBase): - __test__ = True