Saving work in progress on v2 with capa template for annotation problem.
This commit is contained in:
@@ -29,170 +29,53 @@ class AnnotatableModule(XModule):
|
||||
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
|
||||
icon_class = 'annotatable'
|
||||
|
||||
def _is_span(self, element):
|
||||
""" Returns true if the element is a valid annotation span, false otherwise. """
|
||||
return element.get('class') == 'annotatable'
|
||||
|
||||
def _iterspans(self, xmltree, callbacks):
|
||||
""" Iterates over elements and invokes each callback on the span. """
|
||||
|
||||
index = 0
|
||||
for element in xmltree.iter():
|
||||
if self._is_span(element):
|
||||
for callback in callbacks:
|
||||
callback(element, index, xmltree)
|
||||
index += 1
|
||||
|
||||
def _set_span_highlight(self, span, index, xmltree):
|
||||
""" Adds a highlight class to the span. """
|
||||
def _set_annotation_class(self, el):
|
||||
""" Sets the CSS class on the annotation span. """
|
||||
|
||||
cls = ['annotatable-span', 'highlight']
|
||||
marker = self._get_marker_color(span)
|
||||
if marker is not None:
|
||||
cls.append('highlight-'+marker)
|
||||
cls.append('highlight-'+self._get_highlight(el))
|
||||
el.set('class', ' '.join(cls))
|
||||
|
||||
span.set('class', ' '.join(cls))
|
||||
span.tag = 'div'
|
||||
|
||||
def _set_span_comment(self, span, index, xmltree):
|
||||
""" Sets the comment class. """
|
||||
def _set_annotation_data(self, el):
|
||||
""" Transforms the annotation span's xml attributes to HTML data attributes. """
|
||||
|
||||
comment = span.find('comment')
|
||||
if comment is not None:
|
||||
comment.tag = 'div'
|
||||
comment.set('class', 'annotatable-comment')
|
||||
attrs_map = {'body': 'data-comment-body', 'title': 'data-comment-title'}
|
||||
for xml_key in attrs_map.keys():
|
||||
if xml_key in el.attrib:
|
||||
value = el.get(xml_key, '')
|
||||
html_key = attrs_map[xml_key]
|
||||
el.set(html_key, value)
|
||||
del el.attrib[xml_key]
|
||||
|
||||
def _set_span_discussion(self, span, index, xmltree):
|
||||
""" Sets the associated discussion id for the span. """
|
||||
|
||||
if 'discussion' in span.attrib:
|
||||
discussion = span.get('discussion')
|
||||
span.set('data-discussion-id', discussion)
|
||||
del span.attrib['discussion']
|
||||
|
||||
def _set_problem(self, span, index, xmltree):
|
||||
""" Sets the associated problem. """
|
||||
|
||||
problem_el = span.find('problem')
|
||||
if problem_el is not None:
|
||||
problem_id = str(index + 1)
|
||||
problem_el.set('problem_id', problem_id)
|
||||
span.set('data-problem-id', problem_id)
|
||||
parsed_problem = self._parse_problem(problem_el)
|
||||
parsed_problem['discussion_id'] = span.get('data-discussion-id')
|
||||
if parsed_problem is not None:
|
||||
self.problems.append(parsed_problem)
|
||||
span.remove(problem_el)
|
||||
|
||||
def _parse_problem(self, problem_el):
|
||||
""" Returns the problem XML as a dict. """
|
||||
prompt_el = problem_el.find('prompt')
|
||||
answer_el = problem_el.find('answer')
|
||||
tags_el = problem_el.find('tags')
|
||||
|
||||
if any(v is None for v in (prompt_el, answer_el, tags_el)):
|
||||
return None
|
||||
|
||||
tags = []
|
||||
for tag_el in tags_el.iterchildren('tag'):
|
||||
tags.append({
|
||||
'name': tag_el.get('name', ''),
|
||||
'display_name': tag_el.get('display_name', ''),
|
||||
'weight': tag_el.get('weight', 0),
|
||||
'answer': tag_el.get('answer', 'n') == 'y'
|
||||
})
|
||||
|
||||
result = {
|
||||
'problem_id': problem_el.get('problem_id'),
|
||||
'prompt': prompt_el.text,
|
||||
'answer': answer_el.text,
|
||||
'tags': tags
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def _get_marker_color(self, span):
|
||||
def _get_highlight(self, el):
|
||||
""" Returns the name of the marker/highlight color for the span if it is valid, otherwise none."""
|
||||
|
||||
valid_markers = ['yellow', 'orange', 'purple', 'blue', 'green']
|
||||
if 'marker' in span.attrib:
|
||||
marker = span.attrib['marker']
|
||||
del span.attrib['marker']
|
||||
if marker in valid_markers:
|
||||
return marker
|
||||
return None
|
||||
|
||||
def _get_problem_name(self, problem_type):
|
||||
""" Returns the display name for the problem type. Defaults to annotated reading if none given. """
|
||||
problem_types = {
|
||||
'classification': 'Classification Exercise + Guided Discussion',
|
||||
'annotated_reading': 'Annotated Reading + Guided Discussion'
|
||||
}
|
||||
|
||||
if problem_type is not None and problem_type in problem_types.keys():
|
||||
return problem_types[problem_type]
|
||||
return problem_types['annotated_reading']
|
||||
|
||||
valid_highlights = ['yellow', 'orange', 'purple', 'blue', 'green']
|
||||
default_highlight = 'yellow'
|
||||
highlight = el.get('highlight', default_highlight)
|
||||
if highlight in valid_highlights:
|
||||
return highlight
|
||||
return default_highlight
|
||||
|
||||
def _render_content(self):
|
||||
""" Renders annotatable content by transforming spans and adding discussions. """
|
||||
|
||||
callbacks = [
|
||||
self._set_span_highlight,
|
||||
self._set_span_comment,
|
||||
self._set_span_discussion,
|
||||
self._set_problem
|
||||
]
|
||||
|
||||
xmltree = etree.fromstring(self.content)
|
||||
xmltree.tag = 'div'
|
||||
|
||||
self._iterspans(xmltree, callbacks)
|
||||
for el in xmltree.findall('.//annotation'):
|
||||
el.tag = 'div'
|
||||
self._set_annotation_class(el)
|
||||
self._set_annotation_data(el)
|
||||
|
||||
return etree.tostring(xmltree)
|
||||
|
||||
def _render_items(self):
|
||||
items = []
|
||||
|
||||
if self.render_as_problems:
|
||||
discussions = {}
|
||||
for child in self.get_display_items():
|
||||
discussions[child.discussion_id] = child.get_html()
|
||||
|
||||
for problem in self.problems:
|
||||
discussion = None
|
||||
discussion_id = problem['discussion_id']
|
||||
if discussion_id in discussions:
|
||||
discussion = {
|
||||
'discussion_id': discussion_id,
|
||||
'content': discussions[discussion_id]
|
||||
}
|
||||
items.append({
|
||||
'problem': problem,
|
||||
'discussion': discussion
|
||||
})
|
||||
else:
|
||||
for child in self.get_display_items():
|
||||
items.append({ 'discussion': {
|
||||
'discussion_id': child.discussion_id,
|
||||
'content': child.get_html()
|
||||
}})
|
||||
|
||||
return items
|
||||
return etree.tostring(xmltree, encoding='unicode')
|
||||
|
||||
def get_html(self):
|
||||
""" Renders parameters to template. """
|
||||
|
||||
html_content = self._render_content()
|
||||
items = self._render_items()
|
||||
|
||||
context = {
|
||||
'display_name': self.display_name,
|
||||
'problem_name': self.problem_name,
|
||||
'element_id': self.element_id,
|
||||
'html_content': html_content,
|
||||
'render_as_problems': self.render_as_problems,
|
||||
'items': items
|
||||
'discussion_id': self.discussion_id,
|
||||
'content_html': self._render_content()
|
||||
}
|
||||
|
||||
return self.system.render_template('annotatable.html', context)
|
||||
@@ -203,51 +86,16 @@ class AnnotatableModule(XModule):
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
discussion_id = ''
|
||||
if 'discussion' in xmltree.attrib:
|
||||
discussion_id = xmltree.get('discussion')
|
||||
del xmltree.attrib['discussion']
|
||||
|
||||
self.element_id = self.location.html_id();
|
||||
self.content = self.definition['data']
|
||||
self.problem_type = xmltree.get('problem_type')
|
||||
self.render_as_problems = (self.problem_type == 'classification')
|
||||
self.problem_name = self._get_problem_name(self.problem_type)
|
||||
self.problems = []
|
||||
|
||||
self.content = etree.tostring(xmltree, encoding='unicode')
|
||||
self.element_id = self.location.html_id()
|
||||
self.discussion_id = discussion_id
|
||||
|
||||
class AnnotatableDescriptor(RawDescriptor):
|
||||
module_class = AnnotatableModule
|
||||
stores_state = True
|
||||
template_dir_name = "annotatable"
|
||||
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
children = []
|
||||
for child in xml_object.findall('discussion'):
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
|
||||
xml_object.remove(child)
|
||||
except Exception as e:
|
||||
log.exception("Unable to load child when parsing Sequence. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {
|
||||
'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode'),
|
||||
'children': children
|
||||
}
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
try:
|
||||
root = etree.fromstring(self.definition['data'])
|
||||
for child in self.get_children():
|
||||
root.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return root
|
||||
except etree.XMLSyntaxError as err:
|
||||
# Can't recover here, so just add some info and
|
||||
# re-raise
|
||||
lines = self.definition['data'].split('\n')
|
||||
line, offset = err.position
|
||||
msg = ("Unable to create xml for problem {loc}. "
|
||||
"Context: '{context}'".format(
|
||||
context=lines[line - 1][offset - 40:offset + 40],
|
||||
loc=self.location))
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
template_dir_name = "annotatable"
|
||||
@@ -13,7 +13,6 @@
|
||||
border-radius: 3px;
|
||||
.annotatable-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 30px;
|
||||
}
|
||||
.annotatable-help-icon {
|
||||
@@ -25,7 +24,9 @@
|
||||
height: 17px;
|
||||
background: url(../images/info-icon.png) no-repeat;
|
||||
}
|
||||
.annotatable-toggle, .annotatable-help-icon { margin: 2px 7px 2px 0; }
|
||||
.annotatable-toggle, .annotatable-help-icon {
|
||||
margin: 2px 7px 2px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,16 +160,12 @@
|
||||
|
||||
.ui-tooltip.qtip.ui-tooltip-annotatable {
|
||||
max-width: 375px;
|
||||
.ui-tooltip-title:before {
|
||||
font-weight: normal;
|
||||
content: "Guided Discussion: ";
|
||||
}
|
||||
.ui-tooltip-content {
|
||||
padding: 0 10px;
|
||||
.annotatable-comment {
|
||||
display: block;
|
||||
margin: 0px 0px 10px 0;
|
||||
max-height: 225px; // truncate the text via JS so we can get an ellipsis
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
}
|
||||
.annotatable-reply {
|
||||
|
||||
@@ -4,19 +4,13 @@ class @Annotatable
|
||||
wrapperSelector: '.annotatable-wrapper'
|
||||
toggleSelector: '.annotatable-toggle'
|
||||
spanSelector: '.annotatable-span'
|
||||
commentSelector: '.annotatable-comment'
|
||||
replySelector: '.annotatable-reply'
|
||||
helpSelector: '.annotatable-help-icon'
|
||||
returnSelector: '.annotatable-return'
|
||||
problemSelector: '.annotatable-problem'
|
||||
problemSubmitSelector: '.annotatable-problem-submit'
|
||||
problemTagSelector: '.annotatable-problem-tags > li'
|
||||
|
||||
discussionXModuleSelector: '.xmodule_DiscussionModule'
|
||||
discussionSelector: '.discussion-module'
|
||||
|
||||
commentMaxLength: 750 # Max length characters to show in the comment hover state
|
||||
|
||||
constructor: (el) ->
|
||||
console.log 'loaded Annotatable' if @_debug
|
||||
@el = el
|
||||
@@ -28,6 +22,7 @@ class @Annotatable
|
||||
init: () ->
|
||||
@initEvents()
|
||||
@initTips()
|
||||
@initDiscussion()
|
||||
|
||||
initEvents: () ->
|
||||
@annotationsHidden = false
|
||||
@@ -35,13 +30,9 @@ class @Annotatable
|
||||
@$(@wrapperSelector).delegate @replySelector, 'click', @onClickReply
|
||||
$(@discussionXModuleSelector).delegate @returnSelector, 'click', @onClickReturn
|
||||
|
||||
problemSelector = @problemSelector
|
||||
@$(@problemSubmitSelector).bind 'click', (e) ->
|
||||
$(this).closest(problemSelector).next().show()
|
||||
|
||||
@$(@problemTagSelector).bind 'click', (e) ->
|
||||
$(this).toggleClass('selected')
|
||||
|
||||
initDiscussion: () ->
|
||||
1
|
||||
|
||||
initTips: () ->
|
||||
@savedTips = []
|
||||
@$(@spanSelector).each (index, el) => $(el).qtip(@getTipOptions el)
|
||||
@@ -86,13 +77,11 @@ class @Annotatable
|
||||
onClickReply: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
discussion_el = @getInlineDiscussion e.currentTarget
|
||||
return_el = discussion_el.prev(@returnSelector)
|
||||
|
||||
if return_el.length == 1
|
||||
@scrollTo(return_el, () -> @afterScrollToDiscussion(discussion_el))
|
||||
problem_el = @getProblemEl e.currentTarget
|
||||
if problem_el.length == 1
|
||||
@scrollTo(problem_el, @afterScrollToProblem)
|
||||
else
|
||||
@scrollTo(discussion_el, @afterScrollToDiscussion)
|
||||
console.log 'Problem not found! Event: ', e
|
||||
|
||||
onClickReturn: (e) =>
|
||||
e.preventDefault()
|
||||
@@ -103,15 +92,24 @@ class @Annotatable
|
||||
@scrollTo(el, @afterScrollToSpan, offset)
|
||||
|
||||
getSpan: (el) ->
|
||||
discussion_id = @getDiscussionId(el)
|
||||
@$(@spanSelector).filter("[data-discussion-id='#{discussion_id}']")
|
||||
span_id = @getSpanId(el)
|
||||
@$(@spanSelector).filter("[data-span-id='#{span_id}']")
|
||||
|
||||
getInlineDiscussion: (el) ->
|
||||
discussion_id = @getDiscussionId(el)
|
||||
getDiscussion: (el) ->
|
||||
discussion_id = @getDiscussionId()
|
||||
$(@discussionXModuleSelector).find(@discussionSelector).filter("[data-discussion-id='#{discussion_id}']")
|
||||
|
||||
getDiscussionId: (el) ->
|
||||
$(el).data('discussion-id')
|
||||
getProblem: (el) ->
|
||||
el # TODO
|
||||
|
||||
getProblemId: (el) ->
|
||||
$(el).data('problem-id')
|
||||
|
||||
getSpanId: (el) ->
|
||||
$(el).data('span-id')
|
||||
|
||||
getDiscussionId: () ->
|
||||
@$(@wrapperSelector).data('discussion-id')
|
||||
|
||||
toggleAnnotations: () ->
|
||||
hide = (@annotationsHidden = not @annotationsHidden)
|
||||
@@ -144,30 +142,32 @@ class @Annotatable
|
||||
btn = $('.discussion-show', discussion_el)
|
||||
btn.click() if !btn.hasClass('shown')
|
||||
|
||||
afterScrollToProblem: (problem_el) ->
|
||||
problem_el.effect 'highlight', {}, 500
|
||||
|
||||
afterScrollToSpan: (span_el) ->
|
||||
span_el.effect 'highlight', {color: 'rgba(0,0,0,0.5)' }, 1000
|
||||
|
||||
makeTipContent: (el) ->
|
||||
(api) =>
|
||||
discussion_id = @getDiscussionId(el)
|
||||
comment = $(@commentSelector, el).first().clone()
|
||||
text = @_truncate comment.text().trim(), @commentMaxLength
|
||||
comment.text(text)
|
||||
if discussion_id
|
||||
comment = comment.after(@createReplyLink discussion_id)
|
||||
comment
|
||||
text = $(el).data('comment-body')
|
||||
comment = @createCommentEl(text)
|
||||
reply = @createReplyLink('dummy-problem-id')
|
||||
$(comment).add(reply)
|
||||
|
||||
makeTipTitle: (el) ->
|
||||
(api) =>
|
||||
comment = $(@commentSelector, el).first()
|
||||
title = comment.attr('title')
|
||||
title = $(el).data('comment-title')
|
||||
(if title then title else 'Commentary')
|
||||
|
||||
createReplyLink: (discussion_id) ->
|
||||
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-discussion-id=\"#{discussion_id}\">See Full Discussion</a>")
|
||||
createCommentEl: (text) ->
|
||||
$("<div class=\"annotatable-comment\">#{text}</div>")
|
||||
|
||||
createReturnLink: (discussion_id) ->
|
||||
$("<a class=\"annotatable-return\" href=\"javascript:void(0);\" data-discussion-id=\"#{discussion_id}\">Return to annotation</a>")
|
||||
createReplyLink: (problem_id) ->
|
||||
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-problem-id=\"#{problem_id}\">Reply to Annotation</a>")
|
||||
|
||||
createReturnLink: (span_id) ->
|
||||
$("<a class=\"annotatable-return\" href=\"javascript:void(0);\" data-span-id=\"#{span_id}\">Return to annotation</a>")
|
||||
|
||||
openSavedTips: () ->
|
||||
@showTips @savedTips
|
||||
@@ -200,10 +200,4 @@ class @Annotatable
|
||||
done = false
|
||||
return =>
|
||||
fn.call this unless done
|
||||
done = true
|
||||
|
||||
_truncate: (text = '', limit) ->
|
||||
if text.length > limit
|
||||
text.substring(0, limit - 1).split(' ').slice(0, -1).join(' ') + '...' # truncate on word boundary
|
||||
else
|
||||
text
|
||||
done = true
|
||||
Reference in New Issue
Block a user