Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion xblocks_contrib/annotatable/annotatable.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,30 @@
import markupsafe
from django.utils.translation import gettext_noop as _
from lxml import etree
from opaque_keys.edx.keys import UsageKey
from web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.fields import Scope, String, XMLString
from xblock.utils.resources import ResourceLoader

from xblocks_contrib.common.xml_utils import LegacyXmlMixin

log = logging.getLogger(__name__)

resource_loader = ResourceLoader(__name__)


class SerializationError(Exception):
"""
Thrown when a module cannot be exported to XML
"""
def __init__(self, location, msg):
super().__init__(msg)
self.location = location


@XBlock.needs("i18n")
class AnnotatableBlock(XBlock):
class AnnotatableBlock(LegacyXmlMixin, XBlock):
"""
AnnotatableXBlock allows instructors to create annotated content that students can view interactively.
Annotations can be styled and customized, with internationalization support for multilingual environments.
Expand Down Expand Up @@ -84,6 +96,18 @@ class AnnotatableBlock(XBlock):
# List of supported highlight colors for annotations
HIGHLIGHT_COLORS = ["yellow", "orange", "purple", "blue", "green"]

@property
def location(self):
return self.scope_ids.usage_id

@location.setter
def location(self, value):
assert isinstance(value, UsageKey)
self.scope_ids = self.scope_ids._replace(
def_id=value, # Note: assigning a UsageKey as def_id is OK in old mongo / import system but wrong in split
usage_id=value,
)

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.
Expand Down Expand Up @@ -234,3 +258,45 @@ def workbench_scenarios():
""",
),
]

@classmethod
def definition_from_xml(cls, xml_object, system):
if len(xml_object) == 0 and len(list(xml_object.items())) == 0:
return {'data': ''}, []
return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}, []

def definition_to_xml(self, resource_fs):
"""
Return an Element if we've kept the import OLX, or None otherwise.
"""
# If there's no self.data, it means that an XBlock/XModule originally
# existed for this data at the time of import/editing, but was later
# uninstalled. RawDescriptor therefore never got to preserve the
# original OLX that came in, and we have no idea how it should be
# serialized for export. It's possible that we could do some smarter
# fallback here and attempt to extract the data, but it's reasonable
# and simpler to just skip this node altogether.
if not self.data:
log.warning(
"Could not serialize %s: No XBlock installed for '%s' tag.",
self.location,
self.location.block_type,
)
return None

# Normal case: Just echo back the original OLX we saved.
try:
return etree.fromstring(self.data)
except etree.XMLSyntaxError as err:
# Can't recover here, so just add some info and
# re-raise
lines = self.data.split('\n')
line, offset = err.position # lint-amnesty, pylint: disable=unpacking-non-sequence
msg = (
"Unable to create xml for block {loc}. "
"Context: '{context}'"
).format(
context=lines[line - 1][offset - 40:offset + 40],
loc=self.location,
)
raise SerializationError(self.location, msg) from err
2 changes: 1 addition & 1 deletion xblocks_contrib/common/xml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def own_metadata(block: XBlock) -> dict[str, Any]:
keys, mapped to their serialized values
"""
result = {}
for field in block.fields.values(): # lint-amnesty, pylint: disable=no-member
for field in block.fields.values():
if field.scope == Scope.settings and field.is_set_on(block):
try:
result[field.name] = field.read_json(block)
Expand Down
4 changes: 2 additions & 2 deletions xblocks_contrib/html/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def stringify_children(node):
# This makes our block more resilient. It won't crash in test environments
# where the user service might not be available.
@XBlock.wants("user")
class HtmlBlock(LegacyXmlMixin, XBlock): # pylint: disable=abstract-method
class HtmlBlock(LegacyXmlMixin, XBlock):
"""
The HTML XBlock.
"""
Expand Down Expand Up @@ -268,7 +268,7 @@ def get_html(self):
data = data.replace("%%COURSE_ID%%", str(self.scope_ids.usage_id.context_key))
return data

def studio_view(self, context=None): # pylint: disable=unused-argument
def studio_view(self, context=None):
"""Return a fragment that contains the html for the studio view."""
# Only the ReactJS editor is supported for this block.
# See https://github.com/openedx/frontend-app-authoring/tree/master/src/editors/containers/TextEditor
Expand Down
16 changes: 16 additions & 0 deletions xblocks_contrib/lti/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin

from xblocks_contrib.common.xml_utils import LegacyXmlMixin

from .lti_2_util import LTI20BlockMixin, LTIError

# The anonymous user ID for the user in the course.
Expand Down Expand Up @@ -122,6 +124,8 @@ class LTIFields:

https://github.com/idan/oauthlib/blob/master/oauthlib/oauth1/rfc5849/signature.py#L136
"""
data = String(default='', scope=Scope.content)

display_name = String(
display_name=_("Display Name"),
help=_(
Expand Down Expand Up @@ -290,6 +294,7 @@ class LTIFields:
class LTIBlock(
LTIFields,
LTI20BlockMixin,
LegacyXmlMixin,
StudioEditableXBlockMixin,
XBlock,
):
Expand Down Expand Up @@ -1011,3 +1016,14 @@ def is_past_due(self):
else:
close_date = due_date
return close_date is not None and datetime.datetime.now(UTC) > close_date

@classmethod
def definition_from_xml(cls, xml_object, system):
if len(xml_object) == 0 and len(list(xml_object.items())) == 0:
return {'data': ''}, []
return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}, []

def definition_to_xml(self, resource_fs):
if self.data:
return etree.fromstring(self.data)
return etree.Element(self.category)
Loading