Commit af2bfa9d by Arthur Barrett

Saving work in progress on v2 with capa template for annotation problem.

parent 610e210c
......@@ -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)
span.set('class', ' '.join(cls))
span.tag = 'div'
def _set_span_comment(self, span, index, xmltree):
""" Sets the comment class. """
comment = span.find('comment')
if comment is not None:
comment.tag = 'div'
comment.set('class', 'annotatable-comment')
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
cls.append('highlight-'+self._get_highlight(el))
el.set('class', ' '.join(cls))
def _get_marker_color(self, span):
""" Returns the name of the marker/highlight color for the span if it is valid, otherwise none."""
def _set_annotation_data(self, el):
""" Transforms the annotation span's xml attributes to HTML data attributes. """
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'
}
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]
if problem_type is not None and problem_type in problem_types.keys():
return problem_types[problem_type]
return problem_types['annotated_reading']
def _get_highlight(self, el):
""" Returns the name of the marker/highlight color for the span if it is valid, otherwise none."""
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)
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
for el in xmltree.findall('.//annotation'):
el.tag = 'div'
self._set_annotation_class(el)
self._set_annotation_data(el)
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]
\ No newline at end of file
template_dir_name = "annotatable"
\ No newline at end of file
......@@ -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>")
createReplyLink: (problem_id) ->
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-problem-id=\"#{problem_id}\">Reply to Annotation</a>")
createReturnLink: (discussion_id) ->
$("<a class=\"annotatable-return\" href=\"javascript:void(0);\" data-discussion-id=\"#{discussion_id}\">Return 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
\ No newline at end of file
<%namespace name="annotatable" file="annotatable_problem.html"/>
<div class="annotatable-wrapper" id="${element_id}-wrapper">
<div class="annotatable-wrapper" data-discussion-id="${discussion_id}">
<div class="annotatable-header">
% if display_name is not UNDEFINED and display_name is not None:
<div class="annotatable-title">${display_name} </div>
% endif
<div class="annotatable-description">
${problem_name}
<a href="javascript:void(0)" class="annotatable-toggle">Hide Annotations</a>
Guided Discussion
<a class="annotatable-toggle" href="javascript:void(0)">Hide Annotations</a>
<div class="annotatable-help-icon" title="Move your cursor over the highlighted areas to display annotations. Discuss the annotations in the forums using the link at the bottom of the annotation. You may hide annotations at any time by using the button at the top of the section."></div>
</div>
</div>
<div class="annotatable-content">${html_content}</div>
% if render_as_problems:
<div class="annotatable-problems">
% for item in items:
${annotatable.render_problem(item['problem'],loop.index,len(items))}
% if item['discussion']:
<div class="annotatable-discussion">${item['discussion']['content']}</div>
% endif
% endfor
</div>
% else:
<div class="annotatable-discussions">
% for item in items:
<div class="annotatable-discussion">
<a class="annotatable-return" href="javascript:void(0);" data-discussion-id="${item['discussion']['discussion_id']}">Return to annotation</a>
${item['discussion']['content']}
</div>
% endfor
</div>
% endif
</div>
<div class="annotatable-content">${content_html}</div>
</div>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment