Skip to content

Commit

Permalink
fix: add annotatable code from edx-platform
Browse files Browse the repository at this point in the history
  • Loading branch information
irtazaakram committed Oct 21, 2024
1 parent f238f58 commit c329afa
Show file tree
Hide file tree
Showing 13 changed files with 1,081 additions and 154 deletions.
16 changes: 13 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,19 @@ dev.clean:
-docker rmi $(REPO_NAME)-dev

dev.build:
docker build -t $(REPO_NAME)-dev $(CURDIR)

dev.run: dev.clean dev.build ## Clean, build and run test image
docker build --no-cache -t $(REPO_NAME)-dev $(CURDIR)

check-log:
@if [ ! -d "$(CURDIR)/var" ]; then \
echo "Creating var directory"; \
mkdir -p $(CURDIR)/var; \
fi
@if [ ! -f "$(CURDIR)/var/workbench.log" ]; then \
echo "Creating empty workbench.log"; \
touch $(CURDIR)/var/workbench.log; \
fi

dev.run: dev.clean dev.build check-log
docker run -p 8000:8000 -v $(CURDIR):/usr/local/src/$(REPO_NAME) --name $(REPO_NAME)-dev $(REPO_NAME)-dev

# XBlock directories
Expand Down
13 changes: 13 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
from datetime import datetime
from subprocess import check_call

import django
from django.conf import settings

settings.configure(
USE_I18N=True,
USE_L10N=True,
USE_TZ=True,
LANGUAGE_CODE="en-us",
LANGUAGES=[("en", "English")],
)

django.setup()


def get_version(*file_paths):
"""
Expand Down
1 change: 0 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ commands =

[testenv:docs]
setenv =
DJANGO_SETTINGS_MODULE = translation_settings
PYTHONPATH = {toxinidir}
# Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx.
SPHINXOPTS = -W
Expand Down
266 changes: 192 additions & 74 deletions xblocks_contrib/annotatable/annotatable.py
Original file line number Diff line number Diff line change
@@ -1,104 +1,222 @@
"""TO-DO: Write a description of what this XBlock is."""

from importlib.resources import files

from django.utils import translation
"""
AnnotatableXBlock allows instructors to add interactive annotations to course content.
Annotations can have configurable attributes such as title, body, problem index, and highlight color.
This block enhances the learning experience by enabling students to view embedded comments, questions, or explanations.
The block supports internationalization (i18n) for multilingual courses.
"""

import logging
import textwrap

import markupsafe
from django.utils.translation import gettext_noop as _
from lxml import etree
from web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.fields import Integer, Scope
from xblock.fields import Scope, String, XMLString
from xblock.utils.resources import ResourceLoader

log = logging.getLogger(__name__)

resource_loader = ResourceLoader(__name__)


# This Xblock is just to test the strucutre of xblocks-contrib
@XBlock.needs("i18n")
class AnnotatableBlock(XBlock):
"""
TO-DO: document what your XBlock does.
AnnotatableXBlock allows instructors to create annotated content that students can view interactively.
Annotations can be styled and customized, with internationalization support for multilingual environments.
"""

# Fields are defined on the class. You can access them in your code as
# self.<fieldname>.

# TO-DO: delete count, and define your own fields.
count = Integer(
default=0,
scope=Scope.user_state,
help="A simple counter, to show something happening",
display_name = String(
display_name=_("Display Name"),
help=_("The display name for this component."),
scope=Scope.settings,
default=_("Annotation"),
)

# Indicates that this XBlock has been extracted from edx-platform.
is_extracted = True
data = XMLString(
help=_("XML data for the annotation"),
scope=Scope.content,
default=textwrap.dedent(
markupsafe.Markup(
"""
<annotatable>
<instructions>
<p>Enter your (optional) instructions for the exercise in HTML format.</p>
<p>Annotations are specified by an <code>{}annotation{}</code> tag which may
may have the following attributes:</p>
<ul class="instructions-template">
<li><code>title</code> (optional). Title of the annotation. Defaults to
<i>Commentary</i> if omitted.</li>
<li><code>body</code> (<b>required</b>). Text of the annotation.</li>
<li><code>problem</code> (optional). Numeric index of the problem
associated with this annotation. This is a zero-based index, so the first
problem on the page would have <code>problem="0"</code>.</li>
<li><code>highlight</code> (optional). Possible values: yellow, red,
orange, green, blue, or purple. Defaults to yellow if this attribute is
omitted.</li>
</ul>
</instructions>
<p>Add your HTML with annotation spans here.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<annotation title="My title" body="My comment" highlight="yellow" problem="0">
Ut sodales laoreet est, egestas gravida felis egestas nec.</annotation> Aenean
at volutpat erat. Cras commodo viverra nibh in aliquam.</p>
<p>Nulla facilisi. <annotation body="Basic annotation example." problem="1">
Pellentesque id vestibulum libero.</annotation> 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.</p>
</annotatable>
"""
).format(markupsafe.escape("<"), markupsafe.escape(">"))
),
)

def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
return files(__package__).joinpath(path).read_text(encoding="utf-8")
# List of supported highlight colors for annotations
HIGHLIGHT_COLORS = ["yellow", "orange", "purple", "blue", "green"]

# TO-DO: change this view to display your data your own way.
def student_view(self, context=None):
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.
"""
Create primary view of the AnnotatableBlock, shown to students when viewing courses.

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.
"""
if context:
pass # TO-DO: do something based on the context.

frag = Fragment()
frag.add_content(
resource_loader.render_django_template(
"templates/annotatable.html",
{
"count": self.count,
},
i18n_service=self.runtime.service(self, "i18n"),
)
)

frag.add_css(self.resource_string("static/css/annotatable.css"))
frag.add_javascript(self.resource_string("static/js/src/annotatable.js"))
frag.initialize_js("AnnotatableBlock")

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, consider-using-dict-items
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, consider-using-dict-items
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)
content = etree.tostring(xmltree, encoding="unicode")

xmltree = etree.fromstring(content)
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 <instructions> 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_html = self._extract_instructions(xmltree)
content_html = self._render_content()

context = {
"display_name": self.display_name,
"instructions_html": instructions_html,
"content_html": content_html,
}

return resource_loader.render_template("templates/annotatable.html", context)

def student_view(self, context=None): # pylint: disable=unused-argument
"""Renders the output that a student will see."""
frag = Fragment(self.get_html())
frag.add_css_url(self.runtime.local_resource_url(self, "public/css/annotatable.css"))
frag.add_javascript_url(self.runtime.local_resource_url(self, "public/js/src/annotatable.js"))
frag.initialize_js("Annotatable")
return frag

def studio_view(self, context=None): # pylint: disable=unused-argument
"""Return the studio view."""
frag = Fragment(resource_loader.render_template("templates/annotatable_editor.html", {"data": self.data}))
frag.add_css_url(self.runtime.local_resource_url(self, "public/css/annotatable_editor.css"))
frag.add_javascript_url(self.runtime.local_resource_url(self, "public/js/src/annotatable_editor.js"))
frag.initialize_js("XMLEditingDescriptor")
return frag

# TO-DO: change this handler to perform your own actions. You may need more
# than one handler, or you may not need any handlers at all.
@XBlock.json_handler
def increment_count(self, data, suffix=""):
"""
Increments data. An example handler.
"""
if suffix:
pass # TO-DO: Use the suffix when storing data.
# Just to show data coming in...
assert data["hello"] == "world"
def submit_studio_edits(self, data, suffix=""): # pylint: disable=unused-argument
"""AJAX handler for saving the studio edits."""
display_name = data.get("display_name")
xml_data = data.get("data")

self.count += 1
return {"count": self.count}
if display_name is not None:
self.display_name = display_name
if xml_data is not None:
self.data = xml_data

return {"result": "success"}

# TO-DO: change this to create the scenarios you'd like to see in the
# workbench while developing your XBlock.
@staticmethod
def workbench_scenarios():
"""Create canned scenario for display in the workbench."""
"""Defines scenarios for displaying the XBlock in the XBlock workbench."""
return [
("AnnotatableXBlock", "<_annotatable_extracted/>"),
(
"AnnotatableBlock",
"""<_annotatable_extracted/>
""",
),
(
"Multiple AnnotatableBlock",
"""<vertical_demo>
<_annotatable_extracted/>
<_annotatable_extracted/>
<_annotatable_extracted/>
"Multiple AnnotatableXBlock",
"""
<vertical_demo>
<_annotatable_extracted/>
<_annotatable_extracted/>
<_annotatable_extracted/>
</vertical_demo>
""",
""",
),
]

@staticmethod
def get_dummy():
"""
Generate initial i18n with dummy method.
"""
return translation.gettext_noop("Dummy")
Loading

0 comments on commit c329afa

Please sign in to comment.