Skip to content

Commit

Permalink
chore: add annotatable xblock
Browse files Browse the repository at this point in the history
  • Loading branch information
irtazaakram committed Jul 24, 2024
1 parent 1ae58cb commit 98a068e
Show file tree
Hide file tree
Showing 16 changed files with 968 additions and 0 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def package_data(pkg, roots, sub_roots):
entry_points={
"xblock.v1": [
# _xblock suffix is added for testing only.
"annotatable_xblock = xblocks_contrib:AnnotatableXBlock",
"poll_xblock = xblocks_contrib:PollXBlock",
]
},
Expand Down
Empty file added tests/test_annotatable.py
Empty file.
1 change: 1 addition & 0 deletions xblocks_contrib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Init for the xblocks_contrib package.
"""

from .annotatable import AnnotatableXBlock
from .poll import PollXBlock

__version__ = "0.1.0"
8 changes: 8 additions & 0 deletions xblocks_contrib/annotatable/.tx/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com

[o:open-edx:p:p:xblocks:r:annotatable]
file_filter = annotatable/translations/<lang>/LC_MESSAGES/text.po
source_file = annotatable/translations/en/LC_MESSAGES/text.po
source_lang = en
type = PO
5 changes: 5 additions & 0 deletions xblocks_contrib/annotatable/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Init for the AnnotatableXBlock package.
"""

from .annotatable import AnnotatableXBlock
207 changes: 207 additions & 0 deletions xblocks_contrib/annotatable/annotatable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import textwrap
from importlib.resources import files

import markupsafe
from lxml import etree
from web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.utils.resources import ResourceLoader
from xblock.utils.studio_editable import StudioEditableXBlockMixin

resource_loader = ResourceLoader(__name__)


def _(text):
"""Make '_' a no-op, so we can scrape strings"""
return text


class AnnotatableXBlock(StudioEditableXBlockMixin, XBlock):
"""
Annotatable XBlock.
"""

display_name = String(
display_name=_("Display Name"),
help=_("The display name for this component."),
scope=Scope.settings,
default=_("Annotation"),
)

data = String(
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(">"))
),
)

instructions_html = String(
help=_("Instructions HTML"),
scope=Scope.user_state,
default="",
enforce_type=True
)

content_html = String(
help=_("Content HTML"),
scope=Scope.user_state,
default="",
enforce_type=True
)

editable_fields = ["display_name"]
HIGHLIGHT_COLORS = ["yellow", "orange", "purple", "blue", "green"]

def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
return files(__package__).joinpath(path).read_text()

Check warning on line 77 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L77

Added line #L77 was not covered by tests

def _get_annotation_class_attr(self, index, el):
"""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)

Check warning on line 87 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L84-L87

Added lines #L84 - L87 were not covered by tests

if color is not None:
if color in self.HIGHLIGHT_COLORS:
cls.append("highlight-" + color)
attr["_delete"] = highlight_key
attr["value"] = " ".join(cls)

Check warning on line 93 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L91-L93

Added lines #L91 - L93 were not covered by tests

return {"class": attr}

Check warning on line 95 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L95

Added line #L95 was not covered by tests

def _get_annotation_data_attr(self, index, el):
"""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 = {

Check warning on line 105 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L104-L105

Added lines #L104 - L105 were not covered by tests
"body": "data-comment-body",
"title": "data-comment-title",
"problem": "data-problem-id",
}

for xml_key, html_key in attrs_map.items():
if xml_key in el.attrib:
value = el.get(xml_key, "")
data_attrs[html_key] = {"value": value, "_delete": xml_key}

Check warning on line 114 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L113-L114

Added lines #L113 - L114 were not covered by tests

return data_attrs

Check warning on line 116 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L116

Added line #L116 was not covered by tests

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))

Check warning on line 122 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L120-L122

Added lines #L120 - L122 were not covered by tests

el.tag = "span"

Check warning on line 124 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L124

Added line #L124 was not covered by tests

for key, value_dict in attr.items():
el.set(key, value_dict["value"])

Check warning on line 127 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L127

Added line #L127 was not covered by tests
if "_delete" in value_dict and value_dict["_delete"] is not None:
delete_key = value_dict["_delete"]
del el.attrib[delete_key]

Check warning on line 130 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L129-L130

Added lines #L129 - L130 were not covered by tests

def _render_content(self):
"""Renders annotatable content with annotation spans and returns HTML."""

xmltree = etree.fromstring(self.data)
content = etree.tostring(xmltree, encoding="unicode")

Check warning on line 136 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L135-L136

Added lines #L135 - L136 were not covered by tests

xmltree = etree.fromstring(content)
xmltree.tag = "div"

Check warning on line 139 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L138-L139

Added lines #L138 - L139 were not covered by tests
if "display_name" in xmltree.attrib:
del xmltree.attrib["display_name"]

Check warning on line 141 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L141

Added line #L141 was not covered by tests

index = 0

Check warning on line 143 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L143

Added line #L143 was not covered by tests
for el in xmltree.findall(".//annotation"):
self._render_annotation(index, el)
index += 1

Check warning on line 146 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L145-L146

Added lines #L145 - L146 were not covered by tests

return etree.tostring(xmltree, encoding="unicode")

Check warning on line 148 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L148

Added line #L148 was not covered by tests

def _extract_instructions(self, xmltree):
"""Removes <instructions> from the xmltree and returns them as a string, otherwise None."""
instructions = xmltree.find("instructions")

Check warning on line 152 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L152

Added line #L152 was not covered by tests
if instructions is not None:
instructions.tag = "div"
xmltree.remove(instructions)
return etree.tostring(instructions, encoding="unicode")
return None

Check warning on line 157 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L154-L157

Added lines #L154 - L157 were not covered by tests

def student_view(self, context=None):
"""
Create primary view of the AnnotatableXBlock, shown to students when viewing courses.
"""
if context:
pass # TO-DO: do something based on the context.
xmltree = etree.fromstring(self.data)
self.instructions_html = self._extract_instructions(xmltree)
self.content_html = self._render_content()

Check warning on line 167 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L164-L167

Added lines #L164 - L167 were not covered by tests

html = self.resource_string("static/html/annotatable.html")
frag = Fragment(html.format(self=self))
frag.add_css(self.resource_string("static/css/annotatable.css"))

Check warning on line 171 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L169-L171

Added lines #L169 - L171 were not covered by tests

frag.add_javascript(self.resource_string("static/js/src/annotatable.js"))
frag.initialize_js("AnnotatableXBlock")
return frag

Check warning on line 175 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L173-L175

Added lines #L173 - L175 were not covered by tests

def studio_view(self, _context=None):
"""
Return the studio view.
"""
html = self.resource_string("static/html/annotatable_editor.html")
frag = Fragment(html.format(self=self))
frag.add_css(self.resource_string("static/css/annotatable_editor.css"))

Check warning on line 183 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L181-L183

Added lines #L181 - L183 were not covered by tests

frag.add_javascript(self.resource_string("static/js/src/annotatable_editor.js"))
frag.initialize_js("XMLEditor")
return frag

Check warning on line 187 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L185-L187

Added lines #L185 - L187 were not covered by tests

@staticmethod
def workbench_scenarios():
"""Create canned scenario for display in the workbench."""
return [

Check warning on line 192 in xblocks_contrib/annotatable/annotatable.py

View check run for this annotation

Codecov / codecov/patch

xblocks_contrib/annotatable/annotatable.py#L192

Added line #L192 was not covered by tests
(
"AnnotatableXBlock",
"""<annotatable_xblock/>
""",
),
(
"Multiple AnnotatableXBlock",
"""<vertical_demo>
<annotatable_xblock/>
<annotatable_xblock/>
<annotatable_xblock/>
</vertical_demo>
""",
),
]
Empty file.
93 changes: 93 additions & 0 deletions xblocks_contrib/annotatable/conf/locale/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Configuration for i18n workflow.

locales:
- en # English - Source Language
# - am # Amharic
- ar # Arabic
# - az # Azerbaijani
# - bg_BG # Bulgarian (Bulgaria)
# - bn_BD # Bengali (Bangladesh)
# - bn_IN # Bengali (India)
# - bs # Bosnian
# - ca # Catalan
# - ca@valencia # Catalan (Valencia)
# - cs # Czech
# - cy # Welsh
# - da # Danish
# - de_DE # German (Germany)
# - el # Greek
# - en_GB # English (United Kingdom)
# # Don't pull these until we figure out why pages randomly display in these locales,
# # when the user's browser is in English and the user is not logged in.
# #- en@lolcat # LOLCAT English
# #- en@pirate # Pirate English
- es_419 # Spanish (Latin America)
# - es_AR # Spanish (Argentina)
# - es_EC # Spanish (Ecuador)
# - es_ES # Spanish (Spain)
# - es_MX # Spanish (Mexico)
# - es_PE # Spanish (Peru)
# - et_EE # Estonian (Estonia)
# - eu_ES # Basque (Spain)
# - fa # Persian
# - fa_IR # Persian (Iran)
# - fi_FI # Finnish (Finland)
# - fil # Filipino
- fr # French
# - gl # Galician
# - gu # Gujarati
- he # Hebrew
- hi # Hindi
# - hr # Croatian
# - hu # Hungarian
# - hy_AM # Armenian (Armenia)
# - id # Indonesian
# - it_IT # Italian (Italy)
# - ja_JP # Japanese (Japan)
# - kk_KZ # Kazakh (Kazakhstan)
# - km_KH # Khmer (Cambodia)
# - kn # Kannada
- ko_KR # Korean (Korea)
# - lt_LT # Lithuanian (Lithuania)
# - ml # Malayalam
# - mn # Mongolian
# - mr # Marathi
# - ms # Malay
# - nb # Norwegian Bokmål
# - ne # Nepali
# - nl_NL # Dutch (Netherlands)
# - or # Oriya
# - pl # Polish
- pt_BR # Portuguese (Brazil)
# - pt_PT # Portuguese (Portugal)
# - ro # Romanian
- ru # Russian
# - si # Sinhala
# - sk # Slovak
# - sl # Slovenian
# - sq # Albanian
# - sr # Serbian
# - sv # Swedish
# - sw # Swahili
# - ta # Tamil
# - te # Telugu
# - th # Thai
# - tr_TR # Turkish (Turkey)
# - uk # Ukranian
# - ur # Urdu
# - uz # Uzbek
# - vi # Vietnamese
- zh_CN # Chinese (China)
# - zh_HK # Chinese (Hong Kong)
# - zh_TW # Chinese (Taiwan)


# The locales used for fake-accented English, for testing.
dummy_locales:
- eo
- rtl # Fake testing language for Arabic

# Directories we don't search for strings.
ignore_dirs:
- '*/css'
- 'public/js/translations'
19 changes: 19 additions & 0 deletions xblocks_contrib/annotatable/static/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
This static directory is for files that should be included in your kit as plain
static files.

You can ask the runtime for a URL that will retrieve these files with:

url = self.runtime.local_resource_url(self, "static/js/lib.js")

The default implementation is very strict though, and will not serve files from
the static directory. It will serve files from a directory named "public".
Create a directory alongside this one named "public", and put files there.
Then you can get a url with code like this:

url = self.runtime.local_resource_url(self, "public/js/lib.js")

The sample code includes a function you can use to read the content of files
in the static directory, like this:

frag.add_javascript(self.resource_string("static/js/my_block.js"))

Loading

0 comments on commit 98a068e

Please sign in to comment.