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): ...@@ -29,170 +29,53 @@ class AnnotatableModule(XModule):
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable' icon_class = 'annotatable'
def _is_span(self, element): def _set_annotation_class(self, el):
""" Returns true if the element is a valid annotation span, false otherwise. """ """ Sets the CSS class on the annotation span. """
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. """
cls = ['annotatable-span', 'highlight'] cls = ['annotatable-span', 'highlight']
marker = self._get_marker_color(span) cls.append('highlight-'+self._get_highlight(el))
if marker is not None: el.set('class', ' '.join(cls))
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 def _set_annotation_data(self, el):
""" Transforms the annotation span's xml attributes to HTML data attributes. """
def _get_marker_color(self, span): attrs_map = {'body': 'data-comment-body', 'title': 'data-comment-title'}
""" Returns the name of the marker/highlight color for the span if it is valid, otherwise none.""" 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]
valid_markers = ['yellow', 'orange', 'purple', 'blue', 'green'] def _get_highlight(self, el):
if 'marker' in span.attrib: """ Returns the name of the marker/highlight color for the span if it is valid, otherwise none."""
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): def _render_content(self):
""" Renders annotatable content by transforming spans and adding discussions. """ """ 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 = etree.fromstring(self.content)
xmltree.tag = 'div' xmltree.tag = 'div'
self._iterspans(xmltree, callbacks) for el in xmltree.findall('.//annotation'):
el.tag = 'div'
return etree.tostring(xmltree) self._set_annotation_class(el)
self._set_annotation_data(el)
def _render_items(self): return etree.tostring(xmltree, encoding='unicode')
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
def get_html(self): def get_html(self):
""" Renders parameters to template. """ """ Renders parameters to template. """
html_content = self._render_content()
items = self._render_items()
context = { context = {
'display_name': self.display_name, 'display_name': self.display_name,
'problem_name': self.problem_name,
'element_id': self.element_id, 'element_id': self.element_id,
'html_content': html_content, 'discussion_id': self.discussion_id,
'render_as_problems': self.render_as_problems, 'content_html': self._render_content()
'items': items
} }
return self.system.render_template('annotatable.html', context) return self.system.render_template('annotatable.html', context)
...@@ -203,51 +86,16 @@ class AnnotatableModule(XModule): ...@@ -203,51 +86,16 @@ class AnnotatableModule(XModule):
instance_state, shared_state, **kwargs) instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data']) 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 = etree.tostring(xmltree, encoding='unicode')
self.content = self.definition['data'] self.element_id = self.location.html_id()
self.problem_type = xmltree.get('problem_type') self.discussion_id = discussion_id
self.render_as_problems = (self.problem_type == 'classification')
self.problem_name = self._get_problem_name(self.problem_type)
self.problems = []
class AnnotatableDescriptor(RawDescriptor): class AnnotatableDescriptor(RawDescriptor):
module_class = AnnotatableModule module_class = AnnotatableModule
stores_state = True stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
\ No newline at end of file
@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
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
border-radius: 3px; border-radius: 3px;
.annotatable-toggle { .annotatable-toggle {
position: absolute; position: absolute;
top: 0;
right: 30px; right: 30px;
} }
.annotatable-help-icon { .annotatable-help-icon {
...@@ -25,7 +24,9 @@ ...@@ -25,7 +24,9 @@
height: 17px; height: 17px;
background: url(../images/info-icon.png) no-repeat; 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 @@ ...@@ -159,16 +160,12 @@
.ui-tooltip.qtip.ui-tooltip-annotatable { .ui-tooltip.qtip.ui-tooltip-annotatable {
max-width: 375px; max-width: 375px;
.ui-tooltip-title:before {
font-weight: normal;
content: "Guided Discussion: ";
}
.ui-tooltip-content { .ui-tooltip-content {
padding: 0 10px; padding: 0 10px;
.annotatable-comment { .annotatable-comment {
display: block; display: block;
margin: 0px 0px 10px 0; margin: 0px 0px 10px 0;
max-height: 225px; // truncate the text via JS so we can get an ellipsis max-height: 225px;
overflow: auto; overflow: auto;
} }
.annotatable-reply { .annotatable-reply {
......
...@@ -4,19 +4,13 @@ class @Annotatable ...@@ -4,19 +4,13 @@ class @Annotatable
wrapperSelector: '.annotatable-wrapper' wrapperSelector: '.annotatable-wrapper'
toggleSelector: '.annotatable-toggle' toggleSelector: '.annotatable-toggle'
spanSelector: '.annotatable-span' spanSelector: '.annotatable-span'
commentSelector: '.annotatable-comment'
replySelector: '.annotatable-reply' replySelector: '.annotatable-reply'
helpSelector: '.annotatable-help-icon' helpSelector: '.annotatable-help-icon'
returnSelector: '.annotatable-return' returnSelector: '.annotatable-return'
problemSelector: '.annotatable-problem'
problemSubmitSelector: '.annotatable-problem-submit'
problemTagSelector: '.annotatable-problem-tags > li'
discussionXModuleSelector: '.xmodule_DiscussionModule' discussionXModuleSelector: '.xmodule_DiscussionModule'
discussionSelector: '.discussion-module' discussionSelector: '.discussion-module'
commentMaxLength: 750 # Max length characters to show in the comment hover state
constructor: (el) -> constructor: (el) ->
console.log 'loaded Annotatable' if @_debug console.log 'loaded Annotatable' if @_debug
@el = el @el = el
...@@ -28,6 +22,7 @@ class @Annotatable ...@@ -28,6 +22,7 @@ class @Annotatable
init: () -> init: () ->
@initEvents() @initEvents()
@initTips() @initTips()
@initDiscussion()
initEvents: () -> initEvents: () ->
@annotationsHidden = false @annotationsHidden = false
...@@ -35,12 +30,8 @@ class @Annotatable ...@@ -35,12 +30,8 @@ class @Annotatable
@$(@wrapperSelector).delegate @replySelector, 'click', @onClickReply @$(@wrapperSelector).delegate @replySelector, 'click', @onClickReply
$(@discussionXModuleSelector).delegate @returnSelector, 'click', @onClickReturn $(@discussionXModuleSelector).delegate @returnSelector, 'click', @onClickReturn
problemSelector = @problemSelector initDiscussion: () ->
@$(@problemSubmitSelector).bind 'click', (e) -> 1
$(this).closest(problemSelector).next().show()
@$(@problemTagSelector).bind 'click', (e) ->
$(this).toggleClass('selected')
initTips: () -> initTips: () ->
@savedTips = [] @savedTips = []
...@@ -86,13 +77,11 @@ class @Annotatable ...@@ -86,13 +77,11 @@ class @Annotatable
onClickReply: (e) => onClickReply: (e) =>
e.preventDefault() e.preventDefault()
discussion_el = @getInlineDiscussion e.currentTarget problem_el = @getProblemEl e.currentTarget
return_el = discussion_el.prev(@returnSelector) if problem_el.length == 1
@scrollTo(problem_el, @afterScrollToProblem)
if return_el.length == 1
@scrollTo(return_el, () -> @afterScrollToDiscussion(discussion_el))
else else
@scrollTo(discussion_el, @afterScrollToDiscussion) console.log 'Problem not found! Event: ', e
onClickReturn: (e) => onClickReturn: (e) =>
e.preventDefault() e.preventDefault()
...@@ -103,15 +92,24 @@ class @Annotatable ...@@ -103,15 +92,24 @@ class @Annotatable
@scrollTo(el, @afterScrollToSpan, offset) @scrollTo(el, @afterScrollToSpan, offset)
getSpan: (el) -> getSpan: (el) ->
discussion_id = @getDiscussionId(el) span_id = @getSpanId(el)
@$(@spanSelector).filter("[data-discussion-id='#{discussion_id}']") @$(@spanSelector).filter("[data-span-id='#{span_id}']")
getInlineDiscussion: (el) -> getDiscussion: (el) ->
discussion_id = @getDiscussionId(el) discussion_id = @getDiscussionId()
$(@discussionXModuleSelector).find(@discussionSelector).filter("[data-discussion-id='#{discussion_id}']") $(@discussionXModuleSelector).find(@discussionSelector).filter("[data-discussion-id='#{discussion_id}']")
getDiscussionId: (el) -> getProblem: (el) ->
$(el).data('discussion-id') el # TODO
getProblemId: (el) ->
$(el).data('problem-id')
getSpanId: (el) ->
$(el).data('span-id')
getDiscussionId: () ->
@$(@wrapperSelector).data('discussion-id')
toggleAnnotations: () -> toggleAnnotations: () ->
hide = (@annotationsHidden = not @annotationsHidden) hide = (@annotationsHidden = not @annotationsHidden)
...@@ -144,30 +142,32 @@ class @Annotatable ...@@ -144,30 +142,32 @@ class @Annotatable
btn = $('.discussion-show', discussion_el) btn = $('.discussion-show', discussion_el)
btn.click() if !btn.hasClass('shown') btn.click() if !btn.hasClass('shown')
afterScrollToProblem: (problem_el) ->
problem_el.effect 'highlight', {}, 500
afterScrollToSpan: (span_el) -> afterScrollToSpan: (span_el) ->
span_el.effect 'highlight', {color: 'rgba(0,0,0,0.5)' }, 1000 span_el.effect 'highlight', {color: 'rgba(0,0,0,0.5)' }, 1000
makeTipContent: (el) -> makeTipContent: (el) ->
(api) => (api) =>
discussion_id = @getDiscussionId(el) text = $(el).data('comment-body')
comment = $(@commentSelector, el).first().clone() comment = @createCommentEl(text)
text = @_truncate comment.text().trim(), @commentMaxLength reply = @createReplyLink('dummy-problem-id')
comment.text(text) $(comment).add(reply)
if discussion_id
comment = comment.after(@createReplyLink discussion_id)
comment
makeTipTitle: (el) -> makeTipTitle: (el) ->
(api) => (api) =>
comment = $(@commentSelector, el).first() title = $(el).data('comment-title')
title = comment.attr('title')
(if title then title else 'Commentary') (if title then title else 'Commentary')
createReplyLink: (discussion_id) -> createCommentEl: (text) ->
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-discussion-id=\"#{discussion_id}\">See Full Discussion</a>") $("<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) -> createReturnLink: (span_id) ->
$("<a class=\"annotatable-return\" href=\"javascript:void(0);\" data-discussion-id=\"#{discussion_id}\">Return to annotation</a>") $("<a class=\"annotatable-return\" href=\"javascript:void(0);\" data-span-id=\"#{span_id}\">Return to annotation</a>")
openSavedTips: () -> openSavedTips: () ->
@showTips @savedTips @showTips @savedTips
...@@ -201,9 +201,3 @@ class @Annotatable ...@@ -201,9 +201,3 @@ class @Annotatable
return => return =>
fn.call this unless done fn.call this unless done
done = true done = true
\ No newline at end of file
_truncate: (text = '', limit) ->
if text.length > limit
text.substring(0, limit - 1).split(' ').slice(0, -1).join(' ') + '...' # truncate on word boundary
else
text
<%namespace name="annotatable" file="annotatable_problem.html"/> <div class="annotatable-wrapper" data-discussion-id="${discussion_id}">
<div class="annotatable-wrapper" id="${element_id}-wrapper">
<div class="annotatable-header"> <div class="annotatable-header">
% if display_name is not UNDEFINED and display_name is not None: % if display_name is not UNDEFINED and display_name is not None:
<div class="annotatable-title">${display_name} </div> <div class="annotatable-title">${display_name} </div>
% endif % endif
<div class="annotatable-description"> <div class="annotatable-description">
${problem_name} Guided Discussion
<a href="javascript:void(0)" class="annotatable-toggle">Hide Annotations</a> <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 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> </div>
<div class="annotatable-content">${content_html}</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>
\ 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