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

This commit is contained in:
Arthur Barrett
2013-02-20 16:30:19 -05:00
parent 610e210cc8
commit af2bfa9d4e
4 changed files with 83 additions and 267 deletions

View File

@@ -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"

View 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 {

View File

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