Skip to content

Commit c7bb6d6

Browse files
authored
feat: add the pointer tag support for the extracted XBlocks (#118)
* chore: pointer tag support for Annotatable, LTI, WordCloud and Poll XBlocks.
1 parent fae7582 commit c7bb6d6

6 files changed

Lines changed: 122 additions & 410 deletions

File tree

‎xblocks_contrib/annotatable/annotatable.py‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,30 @@
1212
import markupsafe
1313
from django.utils.translation import gettext_noop as _
1414
from lxml import etree
15+
from opaque_keys.edx.keys import UsageKey
1516
from web_fragments.fragment import Fragment
1617
from xblock.core import XBlock
1718
from xblock.fields import Scope, String, XMLString
1819
from xblock.utils.resources import ResourceLoader
1920

21+
from xblocks_contrib.common.xml_utils import LegacyXmlMixin
22+
2023
log = logging.getLogger(__name__)
2124

2225
resource_loader = ResourceLoader(__name__)
2326

2427

28+
class SerializationError(Exception):
29+
"""
30+
Thrown when a module cannot be exported to XML
31+
"""
32+
def __init__(self, location, msg):
33+
super().__init__(msg)
34+
self.location = location
35+
36+
2537
@XBlock.needs("i18n")
26-
class AnnotatableBlock(XBlock):
38+
class AnnotatableBlock(LegacyXmlMixin, XBlock):
2739
"""
2840
AnnotatableXBlock allows instructors to create annotated content that students can view interactively.
2941
Annotations can be styled and customized, with internationalization support for multilingual environments.
@@ -84,6 +96,18 @@ class AnnotatableBlock(XBlock):
8496
# List of supported highlight colors for annotations
8597
HIGHLIGHT_COLORS = ["yellow", "orange", "purple", "blue", "green"]
8698

99+
@property
100+
def location(self):
101+
return self.scope_ids.usage_id
102+
103+
@location.setter
104+
def location(self, value):
105+
assert isinstance(value, UsageKey)
106+
self.scope_ids = self.scope_ids._replace(
107+
def_id=value, # Note: assigning a UsageKey as def_id is OK in old mongo / import system but wrong in split
108+
usage_id=value,
109+
)
110+
87111
def _get_annotation_class_attr(self, index, el): # pylint: disable=unused-argument
88112
"""Returns a dict with the CSS class attribute to set on the annotation
89113
and an XML key to delete from the element.
@@ -234,3 +258,45 @@ def workbench_scenarios():
234258
""",
235259
),
236260
]
261+
262+
@classmethod
263+
def definition_from_xml(cls, xml_object, system):
264+
if len(xml_object) == 0 and len(list(xml_object.items())) == 0:
265+
return {'data': ''}, []
266+
return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}, []
267+
268+
def definition_to_xml(self, resource_fs):
269+
"""
270+
Return an Element if we've kept the import OLX, or None otherwise.
271+
"""
272+
# If there's no self.data, it means that an XBlock/XModule originally
273+
# existed for this data at the time of import/editing, but was later
274+
# uninstalled. RawDescriptor therefore never got to preserve the
275+
# original OLX that came in, and we have no idea how it should be
276+
# serialized for export. It's possible that we could do some smarter
277+
# fallback here and attempt to extract the data, but it's reasonable
278+
# and simpler to just skip this node altogether.
279+
if not self.data:
280+
log.warning(
281+
"Could not serialize %s: No XBlock installed for '%s' tag.",
282+
self.location,
283+
self.location.block_type,
284+
)
285+
return None
286+
287+
# Normal case: Just echo back the original OLX we saved.
288+
try:
289+
return etree.fromstring(self.data)
290+
except etree.XMLSyntaxError as err:
291+
# Can't recover here, so just add some info and
292+
# re-raise
293+
lines = self.data.split('\n')
294+
line, offset = err.position # lint-amnesty, pylint: disable=unpacking-non-sequence
295+
msg = (
296+
"Unable to create xml for block {loc}. "
297+
"Context: '{context}'"
298+
).format(
299+
context=lines[line - 1][offset - 40:offset + 40],
300+
loc=self.location,
301+
)
302+
raise SerializationError(self.location, msg) from err

‎xblocks_contrib/common/xml_utils.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def own_metadata(block: XBlock) -> dict[str, Any]:
132132
keys, mapped to their serialized values
133133
"""
134134
result = {}
135-
for field in block.fields.values(): # lint-amnesty, pylint: disable=no-member
135+
for field in block.fields.values():
136136
if field.scope == Scope.settings and field.is_set_on(block):
137137
try:
138138
result[field.name] = field.read_json(block)

‎xblocks_contrib/html/html.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def stringify_children(node):
143143
# This makes our block more resilient. It won't crash in test environments
144144
# where the user service might not be available.
145145
@XBlock.wants("user")
146-
class HtmlBlock(LegacyXmlMixin, XBlock): # pylint: disable=abstract-method
146+
class HtmlBlock(LegacyXmlMixin, XBlock):
147147
"""
148148
The HTML XBlock.
149149
"""
@@ -268,7 +268,7 @@ def get_html(self):
268268
data = data.replace("%%COURSE_ID%%", str(self.scope_ids.usage_id.context_key))
269269
return data
270270

271-
def studio_view(self, context=None): # pylint: disable=unused-argument
271+
def studio_view(self, context=None):
272272
"""Return a fragment that contains the html for the studio view."""
273273
# Only the ReactJS editor is supported for this block.
274274
# See https://github.com/openedx/frontend-app-authoring/tree/master/src/editors/containers/TextEditor

‎xblocks_contrib/lti/lti.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@
8484
from xblockutils.resources import ResourceLoader
8585
from xblockutils.studio_editable import StudioEditableXBlockMixin
8686

87+
from xblocks_contrib.common.xml_utils import LegacyXmlMixin
88+
8789
from .lti_2_util import LTI20BlockMixin, LTIError
8890

8991
# The anonymous user ID for the user in the course.
@@ -122,6 +124,8 @@ class LTIFields:
122124
123125
https://github.com/idan/oauthlib/blob/master/oauthlib/oauth1/rfc5849/signature.py#L136
124126
"""
127+
data = String(default='', scope=Scope.content)
128+
125129
display_name = String(
126130
display_name=_("Display Name"),
127131
help=_(
@@ -290,6 +294,7 @@ class LTIFields:
290294
class LTIBlock(
291295
LTIFields,
292296
LTI20BlockMixin,
297+
LegacyXmlMixin,
293298
StudioEditableXBlockMixin,
294299
XBlock,
295300
):
@@ -1011,3 +1016,14 @@ def is_past_due(self):
10111016
else:
10121017
close_date = due_date
10131018
return close_date is not None and datetime.datetime.now(UTC) > close_date
1019+
1020+
@classmethod
1021+
def definition_from_xml(cls, xml_object, system):
1022+
if len(xml_object) == 0 and len(list(xml_object.items())) == 0:
1023+
return {'data': ''}, []
1024+
return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}, []
1025+
1026+
def definition_to_xml(self, resource_fs):
1027+
if self.data:
1028+
return etree.fromstring(self.data)
1029+
return etree.Element(self.category)

0 commit comments

Comments
 (0)