Merge branch 'master' into fix/will/mc_checkbox_bug
This commit is contained in:
@@ -46,6 +46,7 @@ setup(
|
||||
"custom_tag_template = xmodule.raw_module:RawDescriptor",
|
||||
"about = xmodule.html_module:AboutDescriptor",
|
||||
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
|
||||
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
|
||||
"foldit = xmodule.foldit_module:FolditDescriptor",
|
||||
]
|
||||
}
|
||||
|
||||
131
common/lib/xmodule/xmodule/annotatable_module.py
Normal file
131
common/lib/xmodule/xmodule/annotatable_module.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class AnnotatableModule(XModule):
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/html/display.coffee'),
|
||||
resource_string(__name__, 'js/src/annotatable/display.coffee')],
|
||||
'js': []
|
||||
}
|
||||
js_module_name = "Annotatable"
|
||||
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
|
||||
icon_class = 'annotatable'
|
||||
|
||||
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)
|
||||
|
||||
if color is not None:
|
||||
if color in self.highlight_colors:
|
||||
cls.append('highlight-'+color)
|
||||
attr['_delete'] = highlight_key
|
||||
attr['value'] = ' '.join(cls)
|
||||
|
||||
return { 'class' : attr }
|
||||
|
||||
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 = {
|
||||
'body': 'data-comment-body',
|
||||
'title': 'data-comment-title',
|
||||
'problem': 'data-problem-id'
|
||||
}
|
||||
|
||||
for xml_key in attrs_map.keys():
|
||||
if xml_key in el.attrib:
|
||||
value = el.get(xml_key, '')
|
||||
html_key = attrs_map[xml_key]
|
||||
data_attrs[html_key] = { 'value': value, '_delete': xml_key }
|
||||
|
||||
return data_attrs
|
||||
|
||||
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))
|
||||
|
||||
el.tag = 'span'
|
||||
|
||||
for key in attr.keys():
|
||||
el.set(key, attr[key]['value'])
|
||||
if '_delete' in attr[key] and attr[key]['_delete'] is not None:
|
||||
delete_key = attr[key]['_delete']
|
||||
del el.attrib[delete_key]
|
||||
|
||||
|
||||
def _render_content(self):
|
||||
""" Renders annotatable content with annotation spans and returns HTML. """
|
||||
xmltree = etree.fromstring(self.content)
|
||||
xmltree.tag = 'div'
|
||||
if 'display_name' in xmltree.attrib:
|
||||
del xmltree.attrib['display_name']
|
||||
|
||||
index = 0
|
||||
for el in xmltree.findall('.//annotation'):
|
||||
self._render_annotation(index, el)
|
||||
index += 1
|
||||
|
||||
return etree.tostring(xmltree, encoding='unicode')
|
||||
|
||||
def _extract_instructions(self, xmltree):
|
||||
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
|
||||
instructions = xmltree.find('instructions')
|
||||
if instructions is not None:
|
||||
instructions.tag = 'div'
|
||||
xmltree.remove(instructions)
|
||||
return etree.tostring(instructions, encoding='unicode')
|
||||
return None
|
||||
|
||||
def get_html(self):
|
||||
""" Renders parameters to template. """
|
||||
context = {
|
||||
'display_name': self.display_name,
|
||||
'element_id': self.element_id,
|
||||
'instructions_html': self.instructions,
|
||||
'content_html': self._render_content()
|
||||
}
|
||||
|
||||
return self.system.render_template('annotatable.html', context)
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
|
||||
self.instructions = self._extract_instructions(xmltree)
|
||||
self.content = etree.tostring(xmltree, encoding='unicode')
|
||||
self.element_id = self.location.html_id()
|
||||
self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
|
||||
|
||||
class AnnotatableDescriptor(RawDescriptor):
|
||||
module_class = AnnotatableModule
|
||||
stores_state = True
|
||||
template_dir_name = "annotatable"
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
@@ -135,8 +135,8 @@ class CapaModule(XModule):
|
||||
self.grace_period = None
|
||||
self.close_date = self.display_due_date
|
||||
|
||||
max_attempts = self.metadata.get('attempts', None)
|
||||
if max_attempts:
|
||||
max_attempts = self.metadata.get('attempts')
|
||||
if max_attempts is not None and max_attempts != '':
|
||||
self.max_attempts = int(max_attempts)
|
||||
else:
|
||||
self.max_attempts = None
|
||||
@@ -247,123 +247,177 @@ class CapaModule(XModule):
|
||||
'progress': Progress.to_js_status_str(self.get_progress())
|
||||
})
|
||||
|
||||
def check_button_name(self):
|
||||
"""
|
||||
Determine the name for the "check" button.
|
||||
Usually it is just "Check", but if this is the student's
|
||||
final attempt, change the name to "Final Check"
|
||||
"""
|
||||
if self.max_attempts is not None:
|
||||
final_check = (self.attempts >= self.max_attempts - 1)
|
||||
else:
|
||||
final_check = False
|
||||
|
||||
return "Final Check" if final_check else "Check"
|
||||
|
||||
def should_show_check_button(self):
|
||||
"""
|
||||
Return True/False to indicate whether to show the "Check" button.
|
||||
"""
|
||||
submitted_without_reset = (self.is_completed() and self.rerandomize == "always")
|
||||
|
||||
# If the problem is closed (past due / too many attempts)
|
||||
# then we do NOT show the "check" button
|
||||
# Also, do not show the "check" button if we're waiting
|
||||
# for the user to reset a randomized problem
|
||||
if self.closed() or submitted_without_reset:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def should_show_reset_button(self):
|
||||
"""
|
||||
Return True/False to indicate whether to show the "Reset" button.
|
||||
"""
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
|
||||
if self.rerandomize in ["always", "onreset"]:
|
||||
|
||||
# If the problem is closed (and not a survey question with max_attempts==0),
|
||||
# then do NOT show the reset button.
|
||||
# If the problem hasn't been submitted yet, then do NOT show
|
||||
# the reset button.
|
||||
if (self.closed() and not is_survey_question) or not self.is_completed():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# Only randomized problems need a "reset" button
|
||||
else:
|
||||
return False
|
||||
|
||||
def should_show_save_button(self):
|
||||
"""
|
||||
Return True/False to indicate whether to show the "Save" button.
|
||||
"""
|
||||
|
||||
# If the user has forced the save button to display,
|
||||
# then show it as long as the problem is not closed
|
||||
# (past due / too many attempts)
|
||||
if self.force_save_button == "true":
|
||||
return not self.closed()
|
||||
else:
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
needs_reset = self.is_completed() and self.rerandomize == "always"
|
||||
|
||||
# If the problem is closed (and not a survey question with max_attempts==0),
|
||||
# then do NOT show the reset button
|
||||
# If we're waiting for the user to reset a randomized problem
|
||||
# then do NOT show the reset button
|
||||
if (self.closed() and not is_survey_question) or needs_reset:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def handle_problem_html_error(self, err):
|
||||
"""
|
||||
Change our problem to a dummy problem containing
|
||||
a warning message to display to users.
|
||||
|
||||
Returns the HTML to show to users
|
||||
|
||||
*err* is the Exception encountered while rendering the problem HTML.
|
||||
"""
|
||||
log.exception(err)
|
||||
|
||||
# TODO (vshnayder): another switch on DEBUG.
|
||||
if self.system.DEBUG:
|
||||
msg = (
|
||||
'[courseware.capa.capa_module] <font size="+1" color="red">'
|
||||
'Failed to generate HTML for problem %s</font>' %
|
||||
(self.location.url()))
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
html = msg
|
||||
|
||||
# We're in non-debug mode, and possibly even in production. We want
|
||||
# to avoid bricking of problem as much as possible
|
||||
else:
|
||||
|
||||
# Presumably, student submission has corrupted LoncapaProblem HTML.
|
||||
# First, pull down all student answers
|
||||
student_answers = self.lcp.student_answers
|
||||
answer_ids = student_answers.keys()
|
||||
|
||||
# Some inputtypes, such as dynamath, have additional "hidden" state that
|
||||
# is not exposed to the student. Keep those hidden
|
||||
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
|
||||
hidden_state_keywords = ['dynamath']
|
||||
for answer_id in answer_ids:
|
||||
for hidden_state_keyword in hidden_state_keywords:
|
||||
if answer_id.find(hidden_state_keyword) >= 0:
|
||||
student_answers.pop(answer_id)
|
||||
|
||||
# Next, generate a fresh LoncapaProblem
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
state=None, # Tabula rasa
|
||||
seed=self.seed, system=self.system)
|
||||
|
||||
# Prepend a scary warning to the student
|
||||
warning = '<div class="capa_reset">'\
|
||||
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
|
||||
'The problem\'s state was corrupted by an invalid submission. ' \
|
||||
'The submission consisted of:'\
|
||||
'<ul>'
|
||||
for student_answer in student_answers.values():
|
||||
if student_answer != '':
|
||||
warning += '<li>' + cgi.escape(student_answer) + '</li>'
|
||||
warning += '</ul>'\
|
||||
'If this error persists, please contact the course staff.'\
|
||||
'</div>'
|
||||
|
||||
html = warning
|
||||
try:
|
||||
html += self.lcp.get_html()
|
||||
except Exception, err: # Couldn't do it. Give up
|
||||
log.exception(err)
|
||||
raise
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def get_problem_html(self, encapsulate=True):
|
||||
'''Return html for the problem. Adds check, reset, save buttons
|
||||
as necessary based on the problem config and state.'''
|
||||
|
||||
try:
|
||||
html = self.lcp.get_html()
|
||||
|
||||
# If we cannot construct the problem HTML,
|
||||
# then generate an error message instead.
|
||||
except Exception, err:
|
||||
log.exception(err)
|
||||
html = self.handle_problem_html_error(err)
|
||||
|
||||
# TODO (vshnayder): another switch on DEBUG.
|
||||
if self.system.DEBUG:
|
||||
msg = (
|
||||
'[courseware.capa.capa_module] <font size="+1" color="red">'
|
||||
'Failed to generate HTML for problem %s</font>' %
|
||||
(self.location.url()))
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
html = msg
|
||||
else:
|
||||
# We're in non-debug mode, and possibly even in production. We want
|
||||
# to avoid bricking of problem as much as possible
|
||||
|
||||
# Presumably, student submission has corrupted LoncapaProblem HTML.
|
||||
# First, pull down all student answers
|
||||
student_answers = self.lcp.student_answers
|
||||
answer_ids = student_answers.keys()
|
||||
|
||||
# Some inputtypes, such as dynamath, have additional "hidden" state that
|
||||
# is not exposed to the student. Keep those hidden
|
||||
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
|
||||
hidden_state_keywords = ['dynamath']
|
||||
for answer_id in answer_ids:
|
||||
for hidden_state_keyword in hidden_state_keywords:
|
||||
if answer_id.find(hidden_state_keyword) >= 0:
|
||||
student_answers.pop(answer_id)
|
||||
|
||||
# Next, generate a fresh LoncapaProblem
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
state=None, # Tabula rasa
|
||||
seed=self.seed, system=self.system)
|
||||
|
||||
# Prepend a scary warning to the student
|
||||
warning = '<div class="capa_reset">'\
|
||||
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
|
||||
'The problem\'s state was corrupted by an invalid submission. ' \
|
||||
'The submission consisted of:'\
|
||||
'<ul>'
|
||||
for student_answer in student_answers.values():
|
||||
if student_answer != '':
|
||||
warning += '<li>' + cgi.escape(student_answer) + '</li>'
|
||||
warning += '</ul>'\
|
||||
'If this error persists, please contact the course staff.'\
|
||||
'</div>'
|
||||
|
||||
html = warning
|
||||
try:
|
||||
html += self.lcp.get_html()
|
||||
except Exception, err: # Couldn't do it. Give up
|
||||
log.exception(err)
|
||||
raise
|
||||
# The convention is to pass the name of the check button
|
||||
# if we want to show a check button, and False otherwise
|
||||
# This works because non-empty strings evaluate to True
|
||||
if self.should_show_check_button():
|
||||
check_button = self.check_button_name()
|
||||
else:
|
||||
check_button = False
|
||||
|
||||
content = {'name': self.display_name,
|
||||
'html': html,
|
||||
'weight': self.descriptor.weight,
|
||||
}
|
||||
|
||||
# We using strings as truthy values, because the terminology of the
|
||||
# check button is context-specific.
|
||||
|
||||
# Put a "Check" button if unlimited attempts or still some left
|
||||
if self.max_attempts is None or self.attempts < self.max_attempts - 1:
|
||||
check_button = "Check"
|
||||
else:
|
||||
# Will be final check so let user know that
|
||||
check_button = "Final Check"
|
||||
|
||||
reset_button = True
|
||||
save_button = True
|
||||
|
||||
# If we're after deadline, or user has exhausted attempts,
|
||||
# question is read-only.
|
||||
if self.closed():
|
||||
check_button = False
|
||||
reset_button = False
|
||||
save_button = False
|
||||
|
||||
# If attempts=0 then show just check and reset buttons; this is for survey questions using capa
|
||||
if self.max_attempts==0:
|
||||
check_button = False
|
||||
reset_button = True
|
||||
save_button = True
|
||||
|
||||
# User submitted a problem, and hasn't reset. We don't want
|
||||
# more submissions.
|
||||
if self.lcp.done and self.rerandomize == "always":
|
||||
check_button = False
|
||||
save_button = False
|
||||
|
||||
# Only show the reset button if pressing it will show different values
|
||||
if self.rerandomize not in ["always", "onreset"]:
|
||||
reset_button = False
|
||||
|
||||
# User hasn't submitted an answer yet -- we don't want resets
|
||||
if not self.lcp.done:
|
||||
reset_button = False
|
||||
|
||||
# We may not need a "save" button if infinite number of attempts and
|
||||
# non-randomized. The problem author can force it. It's a bit weird for
|
||||
# randomization to control this; should perhaps be cleaned up.
|
||||
if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
|
||||
save_button = False
|
||||
|
||||
context = {'problem': content,
|
||||
'id': self.id,
|
||||
'check_button': check_button,
|
||||
'reset_button': reset_button,
|
||||
'save_button': save_button,
|
||||
'reset_button': self.should_show_reset_button(),
|
||||
'save_button': self.should_show_save_button(),
|
||||
'answer_available': self.answer_available(),
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'attempts_used': self.attempts,
|
||||
@@ -396,6 +450,7 @@ class CapaModule(XModule):
|
||||
'problem_save': self.save_problem,
|
||||
'problem_show': self.get_answer,
|
||||
'score_update': self.update_score,
|
||||
'input_ajax': self.lcp.handle_input_ajax
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -419,7 +474,7 @@ class CapaModule(XModule):
|
||||
|
||||
def closed(self):
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
if self.attempts == self.max_attempts:
|
||||
if self.max_attempts is not None and self.attempts >= self.max_attempts:
|
||||
return True
|
||||
if self.is_past_due():
|
||||
return True
|
||||
@@ -528,21 +583,61 @@ class CapaModule(XModule):
|
||||
def make_dict_of_responses(get):
|
||||
'''Make dictionary of student responses (aka "answers")
|
||||
get is POST dictionary.
|
||||
|
||||
The *get* dict has keys of the form 'x_y', which are mapped
|
||||
to key 'y' in the returned dict. For example,
|
||||
'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
|
||||
|
||||
Some inputs always expect a list in the returned dict
|
||||
(e.g. checkbox inputs). The convention is that
|
||||
keys in the *get* dict that end with '[]' will always
|
||||
have list values in the returned dict.
|
||||
For example, if the *get* dict contains {'input_1[]': 'test' }
|
||||
then the output dict would contain {'1': ['test'] }
|
||||
(the value is a list).
|
||||
|
||||
Raises an exception if:
|
||||
|
||||
A key in the *get* dictionary does not contain >= 1 underscores
|
||||
(e.g. "input" is invalid; "input_1" is valid)
|
||||
|
||||
Two keys end up with the same name in the returned dict.
|
||||
(e.g. 'input_1' and 'input_1[]', which both get mapped
|
||||
to 'input_1' in the returned dict)
|
||||
'''
|
||||
answers = dict()
|
||||
for key in get:
|
||||
# e.g. input_resistor_1 ==> resistor_1
|
||||
_, _, name = key.partition('_')
|
||||
|
||||
# This allows for answers which require more than one value for
|
||||
# the same form input (e.g. checkbox inputs). The convention is that
|
||||
# if the name ends with '[]' (which looks like an array), then the
|
||||
# answer will be an array.
|
||||
if not name.endswith('[]'):
|
||||
answers[name] = get[key]
|
||||
# If key has no underscores, then partition
|
||||
# will return (key, '', '')
|
||||
# We detect this and raise an error
|
||||
if name is '':
|
||||
raise ValueError("%s must contain at least one underscore" % str(key))
|
||||
|
||||
else:
|
||||
name = name[:-2]
|
||||
answers[name] = get.getlist(key)
|
||||
# This allows for answers which require more than one value for
|
||||
# the same form input (e.g. checkbox inputs). The convention is that
|
||||
# if the name ends with '[]' (which looks like an array), then the
|
||||
# answer will be an array.
|
||||
is_list_key = name.endswith('[]')
|
||||
name = name[:-2] if is_list_key else name
|
||||
|
||||
if is_list_key:
|
||||
if type(get[key]) is list:
|
||||
val = get[key]
|
||||
else:
|
||||
val = [get[key]]
|
||||
else:
|
||||
val = get[key]
|
||||
|
||||
# If the name already exists, then we don't want
|
||||
# to override it. Raise an error instead
|
||||
if name in answers:
|
||||
raise ValueError("Key %s already exists in answers dict" % str(name))
|
||||
else:
|
||||
answers[name] = val
|
||||
|
||||
return answers
|
||||
|
||||
@@ -550,7 +645,7 @@ class CapaModule(XModule):
|
||||
''' Checks whether answers to a problem are correct, and
|
||||
returns a map of correct/incorrect answers:
|
||||
|
||||
{'success' : bool,
|
||||
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
|
||||
'contents' : html}
|
||||
'''
|
||||
event_info = dict()
|
||||
@@ -609,11 +704,11 @@ class CapaModule(XModule):
|
||||
# 'success' will always be incorrect
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
event_info['attempts'] = self.attempts
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
@@ -663,7 +758,12 @@ class CapaModule(XModule):
|
||||
''' Changes problem state to unfinished -- removes student answers,
|
||||
and causes problem to rerender itself.
|
||||
|
||||
Returns problem html as { 'html' : html-string }.
|
||||
Returns a dictionary of the form:
|
||||
{'success': True/False,
|
||||
'html': Problem HTML string }
|
||||
|
||||
If an error occurs, the dictionary will also have an
|
||||
'error' key containing an error message.
|
||||
'''
|
||||
event_info = dict()
|
||||
event_info['old_state'] = self.lcp.get_state()
|
||||
@@ -686,6 +786,7 @@ class CapaModule(XModule):
|
||||
# reset random number generator seed (note the self.lcp.get_state()
|
||||
# in next line)
|
||||
self.lcp.seed = None
|
||||
|
||||
|
||||
self.lcp = LoncapaProblem(self.definition['data'],
|
||||
self.location.html_id(), self.lcp.get_state(),
|
||||
@@ -694,7 +795,8 @@ class CapaModule(XModule):
|
||||
event_info['new_state'] = self.lcp.get_state()
|
||||
self.system.track_function('reset_problem', event_info)
|
||||
|
||||
return {'html': self.get_problem_html(encapsulate=False)}
|
||||
return { 'success': True,
|
||||
'html': self.get_problem_html(encapsulate=False)}
|
||||
|
||||
|
||||
class CapaDescriptor(RawDescriptor):
|
||||
|
||||
@@ -35,7 +35,8 @@ class StaticContent(object):
|
||||
@staticmethod
|
||||
def compute_location(org, course, name, revision=None, is_thumbnail=False):
|
||||
name = name.replace('/', '_')
|
||||
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail', Location.clean(name), revision])
|
||||
return Location([XASSET_LOCATION_TAG, org, course, 'asset' if not is_thumbnail else 'thumbnail',
|
||||
Location.clean_keeping_underscores(name), revision])
|
||||
|
||||
def get_id(self):
|
||||
return StaticContent.get_id_from_location(self.location)
|
||||
|
||||
@@ -127,6 +127,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# NOTE (THK): This is a last-minute addition for Fall 2012 launch to dynamically
|
||||
# disable the syllabus content for courses that do not provide a syllabus
|
||||
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
|
||||
self._grading_policy = {}
|
||||
self.set_grading_policy(self.definition['data'].get('grading_policy', None))
|
||||
|
||||
self.test_center_exams = []
|
||||
@@ -196,11 +197,9 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
grading_policy.update(course_policy)
|
||||
|
||||
# Here is where we should parse any configurations, so that we can fail early
|
||||
grading_policy['RAW_GRADER'] = grading_policy['GRADER'] # used for cms access
|
||||
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
|
||||
self._grading_policy = grading_policy
|
||||
|
||||
|
||||
# Use setters so that side effecting to .definitions works
|
||||
self.raw_grader = grading_policy['GRADER'] # used for cms access
|
||||
self.grade_cutoffs = grading_policy['GRADE_CUTOFFS']
|
||||
|
||||
@classmethod
|
||||
def read_grading_policy(cls, paths, system):
|
||||
@@ -319,7 +318,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def grader(self):
|
||||
return self._grading_policy['GRADER']
|
||||
return grader_from_conf(self.raw_grader)
|
||||
|
||||
@property
|
||||
def raw_grader(self):
|
||||
|
||||
169
common/lib/xmodule/xmodule/css/annotatable/display.scss
Normal file
169
common/lib/xmodule/xmodule/css/annotatable/display.scss
Normal file
@@ -0,0 +1,169 @@
|
||||
$border-color: #C8C8C8;
|
||||
$body-font-size: em(14);
|
||||
|
||||
.annotatable-header {
|
||||
margin-bottom: .5em;
|
||||
.annotatable-title {
|
||||
font-size: em(22);
|
||||
text-transform: uppercase;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-section {
|
||||
position: relative;
|
||||
padding: .5em 1em;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: .5em;
|
||||
margin-bottom: .5em;
|
||||
|
||||
&.shaded { background-color: #EDEDED; }
|
||||
|
||||
.annotatable-section-title {
|
||||
font-weight: bold;
|
||||
a { font-weight: normal; }
|
||||
}
|
||||
.annotatable-section-body {
|
||||
border-top: 1px solid $border-color;
|
||||
margin-top: .5em;
|
||||
padding-top: .5em;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
ul.instructions-template {
|
||||
list-style: disc;
|
||||
margin-left: 4em;
|
||||
b { font-weight: bold; }
|
||||
i { font-style: italic; }
|
||||
code {
|
||||
display: inline;
|
||||
white-space: pre;
|
||||
font-family: Courier New, monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 2px 1em 2px 0;
|
||||
&.expanded:after { content: " \2191" }
|
||||
&.collapsed:after { content: " \2193" }
|
||||
}
|
||||
|
||||
.annotatable-span {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
|
||||
@each $highlight in (
|
||||
(yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)),
|
||||
(red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
|
||||
(orange rgba(255,165,0,0.3) rgba(255,165,0,0.9)),
|
||||
(green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
|
||||
(blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
|
||||
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
|
||||
|
||||
$marker: nth($highlight,1);
|
||||
$color: nth($highlight,2);
|
||||
$selected_color: nth($highlight,3);
|
||||
|
||||
@if $marker == yellow {
|
||||
&.highlight {
|
||||
background-color: $color;
|
||||
&.selected { background-color: $selected_color; }
|
||||
}
|
||||
}
|
||||
&.highlight-#{$marker} {
|
||||
background-color: $color;
|
||||
&.selected { background-color: $selected_color; }
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
cursor: none;
|
||||
background-color: inherit;
|
||||
.annotatable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.annotatable-comment {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tooltip.qtip.ui-tooltip {
|
||||
font-size: $body-font-size;
|
||||
border: 1px solid #333;
|
||||
border-radius: 1em;
|
||||
background-color: rgba(0,0,0,.85);
|
||||
color: #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
.ui-tooltip-titlebar {
|
||||
font-size: em(16);
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
.ui-tooltip-title {
|
||||
padding: 5px 0px;
|
||||
border-bottom: 2px solid #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ui-tooltip-icon {
|
||||
right: 10px;
|
||||
background: #333;
|
||||
}
|
||||
.ui-state-hover {
|
||||
color: inherit;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
.ui-tooltip-content {
|
||||
color: inherit;
|
||||
font-size: em(14);
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
padding: 0 10px 10px 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
p {
|
||||
color: inherit;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tooltip.qtip.ui-tooltip-annotatable {
|
||||
max-width: 375px;
|
||||
.ui-tooltip-content {
|
||||
padding: 0 10px;
|
||||
.annotatable-comment {
|
||||
display: block;
|
||||
margin: 0px 0px 10px 0;
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
}
|
||||
.annotatable-reply {
|
||||
display: block;
|
||||
border-top: 2px solid #333;
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
height: 0;
|
||||
width: 0;
|
||||
margin-left: -5px;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, .85);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -240,6 +240,15 @@ section.problem {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.partially-correct {
|
||||
@include inline-block();
|
||||
background: url('../images/partially-correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
@@ -811,4 +820,91 @@ section.problem {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.annotation-input {
|
||||
$yellow: rgba(255,255,10,0.3);
|
||||
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 1em;
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
.annotation-header {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: .5em 1em;
|
||||
}
|
||||
.annotation-body { padding: .5em 1em; }
|
||||
a.annotation-return {
|
||||
float: right;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
}
|
||||
a.annotation-return:after { content: " \2191" }
|
||||
|
||||
.block, ul.tags {
|
||||
margin: .5em 0;
|
||||
padding: 0;
|
||||
}
|
||||
.block-highlight {
|
||||
padding: .5em;
|
||||
color: #333;
|
||||
font-style: normal;
|
||||
background-color: $yellow;
|
||||
border: 1px solid darken($yellow, 10%);
|
||||
}
|
||||
.block-comment { font-style: italic; }
|
||||
|
||||
ul.tags {
|
||||
display: block;
|
||||
list-style-type: none;
|
||||
margin-left: 1em;
|
||||
li {
|
||||
display: block;
|
||||
margin: 1em 0 0 0;
|
||||
position: relative;
|
||||
.tag {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgb(102,102,102);
|
||||
margin-left: 40px;
|
||||
&.selected {
|
||||
background-color: $yellow;
|
||||
}
|
||||
}
|
||||
.tag-status {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
.tag-status, .tag { padding: .25em .5em; }
|
||||
}
|
||||
}
|
||||
textarea.comment {
|
||||
$num-lines-to-show: 5;
|
||||
$line-height: 1.4em;
|
||||
$padding: .2em;
|
||||
width: 100%;
|
||||
padding: $padding (2 * $padding);
|
||||
line-height: $line-height;
|
||||
height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2);
|
||||
}
|
||||
.answer-annotation { display: block; margin: 0; }
|
||||
|
||||
/* for debugging the input value field. enable the debug flag on the inputtype */
|
||||
.debug-value {
|
||||
color: #fff;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
background-color: #999;
|
||||
border: 1px solid #000;
|
||||
input[type="text"] { width: 100%; }
|
||||
pre { background-color: #CCC; color: #000; }
|
||||
&:before {
|
||||
display: block;
|
||||
content: "debug input value";
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
common/lib/xmodule/xmodule/css/foldit/leaderboard.scss
Normal file
20
common/lib/xmodule/xmodule/css/foldit/leaderboard.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
$leaderboard: #F4F4F4;
|
||||
|
||||
section.foldit {
|
||||
div.folditchallenge {
|
||||
table {
|
||||
border: 1px solid lighten($leaderboard, 10%);
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
th {
|
||||
background: $leaderboard;
|
||||
color: darken($leaderboard, 25%);
|
||||
}
|
||||
td {
|
||||
background: lighten($leaderboard, 3%);
|
||||
border-bottom: 1px solid #fff;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,27 @@ from xmodule.xml_module import XmlDescriptor
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class FolditModule(XModule):
|
||||
|
||||
css = {'scss': [resource_string(__name__, 'css/foldit/leaderboard.scss')]}
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, descriptor,
|
||||
instance_state, shared_state, **kwargs)
|
||||
# ooh look--I'm lazy, so hardcoding the 7.00x required level.
|
||||
# If we need it generalized, can pull from the xml later
|
||||
self.required_level = 4
|
||||
self.required_sublevel = 5
|
||||
"""
|
||||
|
||||
Example:
|
||||
<foldit show_basic_score="true"
|
||||
required_level="4"
|
||||
required_sublevel="3"
|
||||
show_leaderboard="false"/>
|
||||
"""
|
||||
req_level = self.metadata.get("required_level")
|
||||
req_sublevel = self.metadata.get("required_sublevel")
|
||||
|
||||
# default to what Spring_7012x uses
|
||||
self.required_level = req_level if req_level else 4
|
||||
self.required_sublevel = req_sublevel if req_sublevel else 5
|
||||
|
||||
def parse_due_date():
|
||||
"""
|
||||
@@ -66,6 +79,14 @@ class FolditModule(XModule):
|
||||
PuzzleComplete.completed_puzzles(self.system.anonymous_student_id),
|
||||
key=lambda d: (d['set'], d['subset']))
|
||||
|
||||
def puzzle_leaders(self, n=10):
|
||||
"""
|
||||
Returns a list of n pairs (user, score) corresponding to the top
|
||||
scores; the pairs are in descending order of score.
|
||||
"""
|
||||
from foldit.models import Score
|
||||
|
||||
return [(e['username'], e['score']) for e in Score.get_tops_n(10)]
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
@@ -75,15 +96,48 @@ class FolditModule(XModule):
|
||||
self.required_level,
|
||||
self.required_sublevel)
|
||||
|
||||
showbasic = (self.metadata.get("show_basic_score", "").lower() == "true")
|
||||
showleader = (self.metadata.get("show_leaderboard", "").lower() == "true")
|
||||
|
||||
context = {
|
||||
'due': self.due_str,
|
||||
'success': self.is_complete(),
|
||||
'goal_level': goal_level,
|
||||
'completed': self.completed_puzzles(),
|
||||
'top_scores': self.puzzle_leaders(),
|
||||
'show_basic': showbasic,
|
||||
'show_leader': showleader,
|
||||
'folditbasic': self.get_basicpuzzles_html(),
|
||||
'folditchallenge': self.get_challenge_html()
|
||||
}
|
||||
|
||||
return self.system.render_template('foldit.html', context)
|
||||
|
||||
def get_basicpuzzles_html(self):
|
||||
"""
|
||||
Render html for the basic puzzle section.
|
||||
"""
|
||||
goal_level = '{0}-{1}'.format(
|
||||
self.required_level,
|
||||
self.required_sublevel)
|
||||
|
||||
context = {
|
||||
'due': self.due_str,
|
||||
'success': self.is_complete(),
|
||||
'goal_level': goal_level,
|
||||
'completed': self.completed_puzzles(),
|
||||
}
|
||||
return self.system.render_template('folditbasic.html', context)
|
||||
|
||||
return self.system.render_template('foldit.html', context)
|
||||
def get_challenge_html(self):
|
||||
"""
|
||||
Render html for challenge (i.e., the leaderboard)
|
||||
"""
|
||||
|
||||
context = {
|
||||
'top_scores': self.puzzle_leaders()}
|
||||
|
||||
return self.system.render_template('folditchallenge.html', context)
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
@@ -97,9 +151,10 @@ class FolditModule(XModule):
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
class FolditDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding open ended response questions to courses
|
||||
Module for adding Foldit problems to courses
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = FolditModule
|
||||
@@ -119,6 +174,6 @@ class FolditDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
For now, don't need anything from the xml
|
||||
Get the xml_object's attributes.
|
||||
"""
|
||||
return {}
|
||||
return {'metadata': xml_object.attrib}
|
||||
|
||||
35
common/lib/xmodule/xmodule/js/fixtures/annotatable.html
Normal file
35
common/lib/xmodule/xmodule/js/fixtures/annotatable.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<section class='xmodule_display xmodule_AnnotatableModule' data-type='Annotatable'>
|
||||
<div class="annotatable-wrapper">
|
||||
<div class="annotatable-header">
|
||||
<div class="annotatable-title">First Annotation Exercise</div>
|
||||
</div>
|
||||
<div class="annotatable-section">
|
||||
<div class="annotatable-section-title">
|
||||
Instructions
|
||||
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">Collapse Instructions</a>
|
||||
</div>
|
||||
<div class="annotatable-section-body annotatable-instructions">
|
||||
<div><p>The main goal of this exercise is to start practicing the art of slow reading.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotatable-section">
|
||||
<div class="annotatable-section-title">
|
||||
Guided Discussion
|
||||
<a class="annotatable-toggle annotatable-toggle-annotations" href="javascript:void(0)">Hide Annotations</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="annotatable-content">
|
||||
|87 No, those who are really responsible are Zeus and Fate [Moira] and the Fury [Erinys] who roams in the mist. <br/>
|
||||
|88 <span data-problem-id="0" data-comment-body="Agamemnon says..." class="annotatable-span highlight" data-comment-title="Your Title Here">They are the ones who</span><br/>
|
||||
|100 He [= Zeus], making a formal declaration [eukhesthai], spoke up at a meeting of all the gods and said: <br/>
|
||||
|101 <span data-problem-id="1" data-comment-body="When Zeus speaks..." class="annotatable-span highlight">“hear me, all gods and all goddesses,</span><br/>
|
||||
|113 but he swore a great oath.
|
||||
<span data-problem-id="2" data-comment-body="How is the ‘veering off-course’ ..." class="annotatable-span highlight">And right then and there</span><br/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="problem"><a class="annotation-return" href="javascript:void(0)">Return to Annotation</a></section>
|
||||
<section class="problem"><a class="annotation-return" href="javascript:void(0)">Return to Annotation</a></section>
|
||||
<section class="problem"><a class="annotation-return" href="javascript:void(0)">Return to Annotation</a></section>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
describe 'Annotatable', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'annotatable.html'
|
||||
describe 'constructor', ->
|
||||
el = $('.xmodule_display.xmodule_AnnotatableModule')
|
||||
beforeEach ->
|
||||
@annotatable = new Annotatable(el)
|
||||
it 'works', ->
|
||||
expect(1).toBe(1)
|
||||
197
common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
Normal file
197
common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
Normal file
@@ -0,0 +1,197 @@
|
||||
class @Annotatable
|
||||
_debug: false
|
||||
|
||||
# selectors for the annotatable xmodule
|
||||
toggleAnnotationsSelector: '.annotatable-toggle-annotations'
|
||||
toggleInstructionsSelector: '.annotatable-toggle-instructions'
|
||||
instructionsSelector: '.annotatable-instructions'
|
||||
sectionSelector: '.annotatable-section'
|
||||
spanSelector: '.annotatable-span'
|
||||
replySelector: '.annotatable-reply'
|
||||
|
||||
# these selectors are for responding to events from the annotation capa problem type
|
||||
problemXModuleSelector: '.xmodule_CapaModule'
|
||||
problemSelector: 'section.problem'
|
||||
problemInputSelector: 'section.problem .annotation-input'
|
||||
problemReturnSelector: 'section.problem .annotation-return'
|
||||
|
||||
constructor: (el) ->
|
||||
console.log 'loaded Annotatable' if @_debug
|
||||
@el = el
|
||||
@$el = $(el)
|
||||
@init()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
init: () ->
|
||||
@initEvents()
|
||||
@initTips()
|
||||
|
||||
initEvents: () ->
|
||||
# Initialize toggle handlers for the instructions and annotations sections
|
||||
[@annotationsHidden, @instructionsHidden] = [false, false]
|
||||
@$(@toggleAnnotationsSelector).bind 'click', @onClickToggleAnnotations
|
||||
@$(@toggleInstructionsSelector).bind 'click', @onClickToggleInstructions
|
||||
|
||||
# Initialize handler for 'reply to annotation' events that scroll to
|
||||
# the associated problem. The reply buttons are part of the tooltip
|
||||
# content. It's important that the tooltips be configured to render
|
||||
# as descendants of the annotation module and *not* the document.body.
|
||||
@$el.delegate @replySelector, 'click', @onClickReply
|
||||
|
||||
# Initialize handler for 'return to annotation' events triggered from problems.
|
||||
# 1) There are annotationinput capa problems rendered on the page
|
||||
# 2) Each one has an embedded return link (see annotation capa problem template).
|
||||
# Since the capa problem injects HTML content via AJAX, the best we can do is
|
||||
# is let the click events bubble up to the body and handle them there.
|
||||
$('body').delegate @problemReturnSelector, 'click', @onClickReturn
|
||||
|
||||
initTips: () ->
|
||||
# tooltips are used to display annotations for highlighted text spans
|
||||
@$(@spanSelector).each (index, el) =>
|
||||
$(el).qtip(@getSpanTipOptions el)
|
||||
|
||||
getSpanTipOptions: (el) ->
|
||||
content:
|
||||
title:
|
||||
text: @makeTipTitle(el)
|
||||
text: @makeTipContent(el)
|
||||
position:
|
||||
my: 'bottom center' # of tooltip
|
||||
at: 'top center' # of target
|
||||
target: $(el) # where the tooltip was triggered (i.e. the annotation span)
|
||||
container: @$el
|
||||
adjust:
|
||||
y: -5
|
||||
show:
|
||||
event: 'click mouseenter'
|
||||
solo: true
|
||||
hide:
|
||||
event: 'click mouseleave'
|
||||
delay: 500,
|
||||
fixed: true # don't hide the tooltip if it is moused over
|
||||
style:
|
||||
classes: 'ui-tooltip-annotatable'
|
||||
events:
|
||||
show: @onShowTip
|
||||
|
||||
onClickToggleAnnotations: (e) => @toggleAnnotations()
|
||||
|
||||
onClickToggleInstructions: (e) => @toggleInstructions()
|
||||
|
||||
onClickReply: (e) => @replyTo(e.currentTarget)
|
||||
|
||||
onClickReturn: (e) => @returnFrom(e.currentTarget)
|
||||
|
||||
onShowTip: (event, api) =>
|
||||
event.preventDefault() if @annotationsHidden
|
||||
|
||||
getSpanForProblemReturn: (el) ->
|
||||
problem_id = $(@problemReturnSelector).index(el)
|
||||
@$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
|
||||
|
||||
getProblem: (el) ->
|
||||
problem_id = @getProblemId(el)
|
||||
$(@problemSelector).has(@problemInputSelector).eq(problem_id)
|
||||
|
||||
getProblemId: (el) ->
|
||||
$(el).data('problem-id')
|
||||
|
||||
toggleAnnotations: () ->
|
||||
hide = (@annotationsHidden = not @annotationsHidden)
|
||||
@toggleAnnotationButtonText hide
|
||||
@toggleSpans hide
|
||||
@toggleTips hide
|
||||
|
||||
toggleTips: (hide) ->
|
||||
visible = @findVisibleTips()
|
||||
@hideTips visible
|
||||
|
||||
toggleAnnotationButtonText: (hide) ->
|
||||
buttonText = (if hide then 'Show' else 'Hide')+' Annotations'
|
||||
@$(@toggleAnnotationsSelector).text(buttonText)
|
||||
|
||||
toggleInstructions: () ->
|
||||
hide = (@instructionsHidden = not @instructionsHidden)
|
||||
@toggleInstructionsButton hide
|
||||
@toggleInstructionsText hide
|
||||
|
||||
toggleInstructionsButton: (hide) ->
|
||||
txt = (if hide then 'Expand' else 'Collapse')+' Instructions'
|
||||
cls = (if hide then ['expanded', 'collapsed'] else ['collapsed','expanded'])
|
||||
@$(@toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1])
|
||||
|
||||
toggleInstructionsText: (hide) ->
|
||||
slideMethod = (if hide then 'slideUp' else 'slideDown')
|
||||
@$(@instructionsSelector)[slideMethod]()
|
||||
|
||||
toggleSpans: (hide) ->
|
||||
@$(@spanSelector).toggleClass 'hide', hide, 250
|
||||
|
||||
replyTo: (buttonEl) ->
|
||||
offset = -20
|
||||
el = @getProblem buttonEl
|
||||
if el.length > 0
|
||||
@scrollTo(el, @afterScrollToProblem, offset)
|
||||
else
|
||||
console.log('problem not found. event: ', e) if @_debug
|
||||
|
||||
returnFrom: (buttonEl) ->
|
||||
offset = -200
|
||||
el = @getSpanForProblemReturn buttonEl
|
||||
if el.length > 0
|
||||
@scrollTo(el, @afterScrollToSpan, offset)
|
||||
else
|
||||
console.log('span not found. event:', e) if @_debug
|
||||
|
||||
scrollTo: (el, after, offset = -20) ->
|
||||
$('html,body').scrollTo(el, {
|
||||
duration: 500
|
||||
onAfter: @_once => after?.call this, el
|
||||
offset: offset
|
||||
}) if $(el).length > 0
|
||||
|
||||
afterScrollToProblem: (problem_el) ->
|
||||
problem_el.effect 'highlight', {}, 500
|
||||
|
||||
afterScrollToSpan: (span_el) ->
|
||||
span_el.addClass 'selected', 400, 'swing', ->
|
||||
span_el.removeClass 'selected', 400, 'swing'
|
||||
|
||||
makeTipContent: (el) ->
|
||||
(api) =>
|
||||
text = $(el).data('comment-body')
|
||||
comment = @createComment(text)
|
||||
problem_id = @getProblemId(el)
|
||||
reply = @createReplyLink(problem_id)
|
||||
$(comment).add(reply)
|
||||
|
||||
makeTipTitle: (el) ->
|
||||
(api) =>
|
||||
title = $(el).data('comment-title')
|
||||
(if title then title else 'Commentary')
|
||||
|
||||
createComment: (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>")
|
||||
|
||||
findVisibleTips: () ->
|
||||
visible = []
|
||||
@$(@spanSelector).each (index, el) ->
|
||||
api = $(el).qtip('api')
|
||||
tip = $(api?.elements.tooltip)
|
||||
if tip.is(':visible')
|
||||
visible.push el
|
||||
visible
|
||||
|
||||
hideTips: (elements) ->
|
||||
$(elements).qtip('hide')
|
||||
|
||||
_once: (fn) ->
|
||||
done = false
|
||||
return =>
|
||||
fn.call this unless done
|
||||
done = true
|
||||
@@ -76,6 +76,24 @@ class @Problem
|
||||
# TODO: Some logic to dynamically adjust polling rate based on queuelen
|
||||
window.queuePollerID = window.setTimeout(@poll, 1000)
|
||||
|
||||
|
||||
# Use this if you want to make an ajax call on the input type object
|
||||
# static method so you don't have to instantiate a Problem in order to use it
|
||||
# Input:
|
||||
# url: the AJAX url of the problem
|
||||
# input_id: the input_id of the input you would like to make the call on
|
||||
# NOTE: the id is the ${id} part of "input_${id}" during rendering
|
||||
# If this function is passed the entire prefixed id, the backend may have trouble
|
||||
# finding the correct input
|
||||
# dispatch: string that indicates how this data should be handled by the inputtype
|
||||
# callback: the function that will be called once the AJAX call has been completed.
|
||||
# It will be passed a response object
|
||||
@inputAjax: (url, input_id, dispatch, data, callback) ->
|
||||
data['dispatch'] = dispatch
|
||||
data['input_id'] = input_id
|
||||
$.postWithPrefix "#{url}/input_ajax", data, callback
|
||||
|
||||
|
||||
render: (content) ->
|
||||
if content
|
||||
@el.html(content)
|
||||
|
||||
@@ -71,6 +71,17 @@ class Location(_LocationBase):
|
||||
"""
|
||||
return Location._clean(value, INVALID_CHARS)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def clean_keeping_underscores(value):
|
||||
"""
|
||||
Return value, replacing INVALID_CHARS, but not collapsing multiple '_' chars.
|
||||
This for cleaning asset names, as the YouTube ID's may have underscores in them, and we need the
|
||||
transcript asset name to match. In the future we may want to change the behavior of _clean.
|
||||
"""
|
||||
return INVALID_CHARS.sub('_', value)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def clean_for_url_name(value):
|
||||
"""
|
||||
|
||||
@@ -64,7 +64,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
location = Location(location)
|
||||
json_data = self.module_data.get(location)
|
||||
if json_data is None:
|
||||
return self.modulestore.get_item(location)
|
||||
module = self.modulestore.get_item(location)
|
||||
if module is not None:
|
||||
# update our own cache after going to the DB to get cache miss
|
||||
self.module_data.update(module.system.module_data)
|
||||
return module
|
||||
else:
|
||||
# load the module and apply the inherited metadata
|
||||
try:
|
||||
|
||||
@@ -5,132 +5,132 @@ from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
|
||||
def clone_course(modulestore, contentstore, source_location, dest_location, delete_original=False):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
|
||||
# check to see if the dest_location exists as an empty course
|
||||
# we need an empty course because the app layers manage the permissions and users
|
||||
if not modulestore.has_item(dest_location):
|
||||
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
|
||||
# check to see if the dest_location exists as an empty course
|
||||
# we need an empty course because the app layers manage the permissions and users
|
||||
if not modulestore.has_item(dest_location):
|
||||
raise Exception("An empty course at {0} must have already been created. Aborting...".format(dest_location))
|
||||
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
|
||||
# verify that the dest_location really is an empty course, which means only one
|
||||
dest_modules = modulestore.get_items([dest_location.tag, dest_location.org, dest_location.course, None, None, None])
|
||||
|
||||
if len(dest_modules) != 1:
|
||||
raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))
|
||||
if len(dest_modules) != 1:
|
||||
raise Exception("Course at destination {0} is not an empty course. You can only clone into an empty course. Aborting...".format(dest_location))
|
||||
|
||||
# check to see if the source course is actually there
|
||||
if not modulestore.has_item(source_location):
|
||||
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
|
||||
# check to see if the source course is actually there
|
||||
if not modulestore.has_item(source_location):
|
||||
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
|
||||
|
||||
# Get all modules under this namespace which is (tag, org, course) tuple
|
||||
# Get all modules under this namespace which is (tag, org, course) tuple
|
||||
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
original_loc = Location(module.location)
|
||||
for module in modules:
|
||||
original_loc = Location(module.location)
|
||||
|
||||
if original_loc.category != 'course':
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
else:
|
||||
# on the course module we also have to update the module name
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course, name=dest_location.name)
|
||||
if original_loc.category != 'course':
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
else:
|
||||
# on the course module we also have to update the module name
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course, name=dest_location.name)
|
||||
|
||||
print "Cloning module {0} to {1}....".format(original_loc, module.location)
|
||||
print "Cloning module {0} to {1}....".format(original_loc, module.location)
|
||||
|
||||
if 'data' in module.definition:
|
||||
modulestore.update_item(module.location, module.definition['data'])
|
||||
if 'data' in module.definition:
|
||||
modulestore.update_item(module.location, module.definition['data'])
|
||||
|
||||
# repoint children
|
||||
if 'children' in module.definition:
|
||||
new_children = []
|
||||
for child_loc_url in module.definition['children']:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
new_children = new_children + [child_loc.url()]
|
||||
# repoint children
|
||||
if 'children' in module.definition:
|
||||
new_children = []
|
||||
for child_loc_url in module.definition['children']:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
new_children = new_children + [child_loc.url()]
|
||||
|
||||
modulestore.update_children(module.location, new_children)
|
||||
modulestore.update_children(module.location, new_children)
|
||||
|
||||
# save metadata
|
||||
modulestore.update_metadata(module.location, module.metadata)
|
||||
# save metadata
|
||||
modulestore.update_metadata(module.location, module.metadata)
|
||||
|
||||
# now iterate through all of the assets and clone them
|
||||
# first the thumbnails
|
||||
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
content = contentstore.find(thumb_loc)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
# now iterate through all of the assets and clone them
|
||||
# first the thumbnails
|
||||
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
content = contentstore.find(thumb_loc)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
|
||||
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
|
||||
|
||||
contentstore.save(content)
|
||||
contentstore.save(content)
|
||||
|
||||
# now iterate through all of the assets, also updating the thumbnail pointer
|
||||
# now iterate through all of the assets, also updating the thumbnail pointer
|
||||
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
# be sure to update the pointer to the thumbnail
|
||||
if content.thumbnail_location is not None:
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
# be sure to update the pointer to the thumbnail
|
||||
if content.thumbnail_location is not None:
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
|
||||
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
|
||||
|
||||
contentstore.save(content)
|
||||
contentstore.save(content)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def delete_course(modulestore, contentstore, source_location, commit = False):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
|
||||
# check to see if the source course is actually there
|
||||
if not modulestore.has_item(source_location):
|
||||
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
|
||||
# check to see if the source course is actually there
|
||||
if not modulestore.has_item(source_location):
|
||||
raise Exception("Cannot find a course at {0}. Aborting".format(source_location))
|
||||
|
||||
# first delete all of the thumbnails
|
||||
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
id = StaticContent.get_id_from_location(thumb_loc)
|
||||
print "Deleting {0}...".format(id)
|
||||
# first delete all of the thumbnails
|
||||
thumbs = contentstore.get_all_content_thumbnails_for_course(source_location)
|
||||
for thumb in thumbs:
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
id = StaticContent.get_id_from_location(thumb_loc)
|
||||
print "Deleting {0}...".format(id)
|
||||
if commit:
|
||||
contentstore.delete(id)
|
||||
|
||||
# then delete all of the assets
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
id = StaticContent.get_id_from_location(asset_loc)
|
||||
print "Deleting {0}...".format(id)
|
||||
if commit:
|
||||
contentstore.delete(id)
|
||||
|
||||
# then delete all course modules
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
print "Deleting {0}...".format(module.location)
|
||||
if commit:
|
||||
modulestore.delete_item(module.location)
|
||||
|
||||
# finally delete the top-level course module itself
|
||||
print "Deleting {0}...".format(source_location)
|
||||
if commit:
|
||||
contentstore.delete(id)
|
||||
modulestore.delete_item(source_location)
|
||||
|
||||
# then delete all of the assets
|
||||
assets = contentstore.get_all_content_for_course(source_location)
|
||||
for asset in assets:
|
||||
asset_loc = Location(asset["_id"])
|
||||
id = StaticContent.get_id_from_location(asset_loc)
|
||||
print "Deleting {0}...".format(id)
|
||||
if commit:
|
||||
contentstore.delete(id)
|
||||
|
||||
# then delete all course modules
|
||||
modules = modulestore.get_items([source_location.tag, source_location.org, source_location.course, None, None, None])
|
||||
|
||||
for module in modules:
|
||||
if module.category != 'course': # save deleting the course module for last
|
||||
print "Deleting {0}...".format(module.location)
|
||||
if commit:
|
||||
modulestore.delete_item(module.location)
|
||||
|
||||
# finally delete the top-level course module itself
|
||||
print "Deleting {0}...".format(source_location)
|
||||
if commit:
|
||||
modulestore.delete_item(source_location)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
@@ -7,47 +7,47 @@ from json import dumps
|
||||
|
||||
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
|
||||
|
||||
course = modulestore.get_item(course_location)
|
||||
course = modulestore.get_item(course_location)
|
||||
|
||||
fs = OSFS(root_dir)
|
||||
export_fs = fs.makeopendir(course_dir)
|
||||
fs = OSFS(root_dir)
|
||||
export_fs = fs.makeopendir(course_dir)
|
||||
|
||||
xml = course.export_to_xml(export_fs)
|
||||
with export_fs.open('course.xml', 'w') as course_xml:
|
||||
course_xml.write(xml)
|
||||
xml = course.export_to_xml(export_fs)
|
||||
with export_fs.open('course.xml', 'w') as course_xml:
|
||||
course_xml.write(xml)
|
||||
|
||||
# export the static assets
|
||||
contentstore.export_all_for_course(course_location, root_dir + '/' + course_dir + '/static/')
|
||||
# export the static assets
|
||||
contentstore.export_all_for_course(course_location, root_dir + '/' + course_dir + '/static/')
|
||||
|
||||
# export the static tabs
|
||||
export_extra_content(export_fs, modulestore, course_location, 'static_tab', 'tabs', '.html')
|
||||
# export the static tabs
|
||||
export_extra_content(export_fs, modulestore, course_location, 'static_tab', 'tabs', '.html')
|
||||
|
||||
# export the custom tags
|
||||
export_extra_content(export_fs, modulestore, course_location, 'custom_tag_template', 'custom_tags')
|
||||
# export the custom tags
|
||||
export_extra_content(export_fs, modulestore, course_location, 'custom_tag_template', 'custom_tags')
|
||||
|
||||
# export the course updates
|
||||
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
|
||||
# export the course updates
|
||||
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
|
||||
|
||||
# export the grading policy
|
||||
policies_dir = export_fs.makeopendir('policies')
|
||||
course_run_policy_dir = policies_dir.makeopendir(course.location.name)
|
||||
if 'grading_policy' in course.definition['data']:
|
||||
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
|
||||
grading_policy.write(dumps(course.definition['data']['grading_policy']))
|
||||
# export the grading policy
|
||||
policies_dir = export_fs.makeopendir('policies')
|
||||
course_run_policy_dir = policies_dir.makeopendir(course.location.name)
|
||||
if 'grading_policy' in course.definition['data']:
|
||||
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
|
||||
grading_policy.write(dumps(course.definition['data']['grading_policy']))
|
||||
|
||||
# export all of the course metadata in policy.json
|
||||
with course_run_policy_dir.open('policy.json', 'w') as course_policy:
|
||||
policy = {}
|
||||
policy = {'course/' + course.location.name: course.metadata}
|
||||
course_policy.write(dumps(policy))
|
||||
# export all of the course metadata in policy.json
|
||||
with course_run_policy_dir.open('policy.json', 'w') as course_policy:
|
||||
policy = {}
|
||||
policy = {'course/' + course.location.name: course.metadata}
|
||||
course_policy.write(dumps(policy))
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
|
||||
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
|
||||
items = modulestore.get_items(query_loc)
|
||||
|
||||
if len(items) > 0:
|
||||
item_dir = export_fs.makeopendir(dirname)
|
||||
for item in items:
|
||||
with item_dir.open(item.location.name + file_suffix, 'w') as item_file:
|
||||
item_file.write(item.definition['data'].encode('utf8'))
|
||||
if len(items) > 0:
|
||||
item_dir = export_fs.makeopendir(dirname)
|
||||
for item in items:
|
||||
with item_dir.open(item.location.name + file_suffix, 'w') as item_file:
|
||||
item_file.write(item.definition['data'].encode('utf8'))
|
||||
|
||||
@@ -79,6 +79,9 @@ class CombinedOpenEndedV1Module():
|
||||
INTERMEDIATE_DONE = 'intermediate_done'
|
||||
DONE = 'done'
|
||||
|
||||
#Where the templates live for this problem
|
||||
TEMPLATE_DIR = "combinedopenended"
|
||||
|
||||
def __init__(self, system, location, definition, descriptor,
|
||||
instance_state=None, shared_state=None, metadata = None, static_data = None, **kwargs):
|
||||
|
||||
@@ -343,7 +346,7 @@ class CombinedOpenEndedV1Module():
|
||||
Output: rendered html
|
||||
"""
|
||||
context = self.get_context()
|
||||
html = self.system.render_template('combined_open_ended.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
def get_html_nonsystem(self):
|
||||
@@ -354,7 +357,7 @@ class CombinedOpenEndedV1Module():
|
||||
Output: HTML rendered directly via Mako
|
||||
"""
|
||||
context = self.get_context()
|
||||
html = self.system.render_template('combined_open_ended.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
def get_html_base(self):
|
||||
@@ -531,7 +534,7 @@ class CombinedOpenEndedV1Module():
|
||||
'task_name' : 'Scored Rubric',
|
||||
'class_name' : 'combined-rubric-container'
|
||||
}
|
||||
html = self.system.render_template('combined_open_ended_results.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_legend(self, get):
|
||||
@@ -543,7 +546,7 @@ class CombinedOpenEndedV1Module():
|
||||
context = {
|
||||
'legend_list' : LEGEND_LIST,
|
||||
}
|
||||
html = self.system.render_template('combined_open_ended_legend.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_results(self, get):
|
||||
@@ -574,7 +577,7 @@ class CombinedOpenEndedV1Module():
|
||||
'submission_id' : ri['submission_ids'][i],
|
||||
}
|
||||
context_list.append(context)
|
||||
feedback_table = self.system.render_template('open_ended_result_table.html', {
|
||||
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
|
||||
'context_list' : context_list,
|
||||
'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT,
|
||||
'human_grader_types' : HUMAN_GRADER_TYPE,
|
||||
@@ -586,7 +589,7 @@ class CombinedOpenEndedV1Module():
|
||||
'task_name' : "Feedback",
|
||||
'class_name' : "result-container",
|
||||
}
|
||||
html = self.system.render_template('combined_open_ended_results.html', context)
|
||||
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_status_ajax(self, get):
|
||||
@@ -700,7 +703,7 @@ class CombinedOpenEndedV1Module():
|
||||
'legend_list' : LEGEND_LIST,
|
||||
'render_via_ajax' : render_via_ajax,
|
||||
}
|
||||
status_html = self.system.render_template("combined_open_ended_status.html", context)
|
||||
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context)
|
||||
|
||||
return status_html
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ class RubricParsingError(Exception):
|
||||
|
||||
class CombinedOpenEndedRubric(object):
|
||||
|
||||
TEMPLATE_DIR = "combinedopenended/openended"
|
||||
|
||||
def __init__ (self, system, view_only = False):
|
||||
self.has_score = False
|
||||
self.view_only = view_only
|
||||
@@ -57,9 +59,9 @@ class CombinedOpenEndedRubric(object):
|
||||
rubric_scores = [cat['score'] for cat in rubric_categories]
|
||||
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
|
||||
max_score = max(max_scores)
|
||||
rubric_template = 'open_ended_rubric.html'
|
||||
rubric_template = '{0}/open_ended_rubric.html'.format(self.TEMPLATE_DIR)
|
||||
if self.view_only:
|
||||
rubric_template = 'open_ended_view_only_rubric.html'
|
||||
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
|
||||
html = self.system.render_template(rubric_template,
|
||||
{'categories': rubric_categories,
|
||||
'has_score': self.has_score,
|
||||
@@ -207,7 +209,7 @@ class CombinedOpenEndedRubric(object):
|
||||
for grader_type in tuple[3]:
|
||||
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
|
||||
|
||||
html = self.system.render_template('open_ended_combined_rubric.html',
|
||||
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
|
||||
{'categories': rubric_categories,
|
||||
'has_score': True,
|
||||
'view_only': True,
|
||||
|
||||
@@ -40,6 +40,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
</openended>
|
||||
"""
|
||||
|
||||
TEMPLATE_DIR = "combinedopenended/openended"
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Sets up the response type.
|
||||
@@ -397,10 +399,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
rubric_scores = rubric_dict['rubric_scores']
|
||||
|
||||
if not response_items['success']:
|
||||
return system.render_template("open_ended_error.html",
|
||||
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
|
||||
{'errors': feedback})
|
||||
|
||||
feedback_template = system.render_template("open_ended_feedback.html", {
|
||||
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
|
||||
'grader_type': response_items['grader_type'],
|
||||
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
|
||||
'feedback': feedback,
|
||||
@@ -558,7 +560,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
@return: Rendered html
|
||||
"""
|
||||
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
|
||||
html = system.render_template('open_ended_evaluation.html', context)
|
||||
html = system.render_template('{0}/open_ended_evaluation.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
def handle_ajax(self, dispatch, get, system):
|
||||
@@ -692,7 +694,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
'accept_file_upload': self.accept_file_upload,
|
||||
'eta_message' : eta_string,
|
||||
}
|
||||
html = system.render_template('open_ended.html', context)
|
||||
html = system.render_template('{0}/open_ended.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
</selfassessment>
|
||||
"""
|
||||
|
||||
TEMPLATE_DIR = "combinedopenended/selfassessment"
|
||||
|
||||
def setup_response(self, system, location, definition, descriptor):
|
||||
"""
|
||||
Sets up the module
|
||||
@@ -68,7 +70,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
'accept_file_upload': self.accept_file_upload,
|
||||
}
|
||||
|
||||
html = system.render_template('self_assessment_prompt.html', context)
|
||||
html = system.render_template('{0}/self_assessment_prompt.html'.format(self.TEMPLATE_DIR), context)
|
||||
return html
|
||||
|
||||
|
||||
@@ -129,7 +131,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
#This is a dev_facing_error
|
||||
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state))
|
||||
|
||||
return system.render_template('self_assessment_rubric.html', context)
|
||||
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
|
||||
|
||||
def get_hint_html(self, system):
|
||||
"""
|
||||
@@ -155,7 +157,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
#This is a dev_facing_error
|
||||
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state))
|
||||
|
||||
return system.render_template('self_assessment_hint.html', context)
|
||||
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
|
||||
|
||||
|
||||
def save_answer(self, get, system):
|
||||
|
||||
@@ -471,6 +471,9 @@ class PeerGradingModule(XModule):
|
||||
#This is a student_facing_error
|
||||
error_text = "Could not get list of problems to peer grade. Please notify course staff."
|
||||
success = False
|
||||
except:
|
||||
log.exception("Could not contact peer grading service.")
|
||||
success = False
|
||||
|
||||
|
||||
def _find_corresponding_module_for_location(location):
|
||||
@@ -524,7 +527,7 @@ class PeerGradingModule(XModule):
|
||||
'''
|
||||
Show individual problem interface
|
||||
'''
|
||||
if get == None or get.get('location') == None:
|
||||
if get is None or get.get('location') is None:
|
||||
if not self.use_for_single_location:
|
||||
#This is an error case, because it must be set to use a single location to be called without get parameters
|
||||
#This is a dev_facing_error
|
||||
@@ -589,7 +592,6 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
'task_xml': dictionary of xml strings,
|
||||
}
|
||||
"""
|
||||
log.debug("In definition")
|
||||
expected_children = []
|
||||
for child in expected_children:
|
||||
if len(xml_object.xpath(child)) == 0:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: 'Annotation'
|
||||
data: |
|
||||
<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>
|
||||
children: []
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Open Ended Response
|
||||
max_attempts: 1
|
||||
max_score: 1
|
||||
is_graded: False
|
||||
version: 1
|
||||
display_name: Open Ended Response
|
||||
skip_spelling_checks: False
|
||||
accept_file_upload: False
|
||||
data: |
|
||||
<combinedopenended>
|
||||
<rubric>
|
||||
<rubric>
|
||||
<category>
|
||||
<description>Category 1</description>
|
||||
<option>
|
||||
The response does not incorporate what is needed for a one response.
|
||||
</option>
|
||||
<option>
|
||||
The response is correct for category 1.
|
||||
</option>
|
||||
</category>
|
||||
</rubric>
|
||||
</rubric>
|
||||
<prompt>
|
||||
<p>Why is the sky blue?</p>
|
||||
</prompt>
|
||||
<task>
|
||||
<selfassessment/>
|
||||
</task>
|
||||
<task>
|
||||
<openended min_score_to_attempt="1" max_score_to_attempt="2">
|
||||
<openendedparam>
|
||||
<initial_display>Enter essay here.</initial_display>
|
||||
<answer_display>This is the answer.</answer_display>
|
||||
<grader_payload>{"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"}</grader_payload>
|
||||
</openendedparam>
|
||||
</openended>
|
||||
</task>
|
||||
</combinedopenended>
|
||||
|
||||
|
||||
children: []
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Peer Grading Interface
|
||||
attempts: 1
|
||||
use_for_single_location: False
|
||||
link_to_location: None
|
||||
is_graded: False
|
||||
max_grade: 1
|
||||
data: |
|
||||
<peergrading>
|
||||
</peergrading>
|
||||
|
||||
children: []
|
||||
@@ -28,21 +28,35 @@ open_ended_grading_interface = {
|
||||
'grading_controller' : 'grading_controller'
|
||||
}
|
||||
|
||||
test_system = ModuleSystem(
|
||||
ajax_url='courses/course_id/modx/a_location',
|
||||
track_function=Mock(),
|
||||
get_module=Mock(),
|
||||
# "render" to just the context...
|
||||
render_template=lambda template, context: str(context),
|
||||
replace_urls=Mock(),
|
||||
user=Mock(is_staff=False),
|
||||
filestore=Mock(),
|
||||
debug=True,
|
||||
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||
anonymous_student_id='student',
|
||||
open_ended_grading_interface= open_ended_grading_interface
|
||||
)
|
||||
|
||||
def test_system():
|
||||
"""
|
||||
Construct a test ModuleSystem instance.
|
||||
|
||||
By default, the render_template() method simply returns
|
||||
the context it is passed as a string.
|
||||
You can override this behavior by monkey patching:
|
||||
|
||||
system = test_system()
|
||||
system.render_template = my_render_func
|
||||
|
||||
where my_render_func is a function of the form
|
||||
my_render_func(template, context)
|
||||
"""
|
||||
return ModuleSystem(
|
||||
ajax_url='courses/course_id/modx/a_location',
|
||||
track_function=Mock(),
|
||||
get_module=Mock(),
|
||||
render_template=lambda template, context: str(context),
|
||||
replace_urls=lambda html: str(html),
|
||||
user=Mock(is_staff=False),
|
||||
filestore=Mock(),
|
||||
debug=True,
|
||||
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||
anonymous_student_id='student',
|
||||
open_ended_grading_interface= open_ended_grading_interface
|
||||
)
|
||||
|
||||
|
||||
class ModelsTest(unittest.TestCase):
|
||||
|
||||
129
common/lib/xmodule/xmodule/tests/test_annotatable_module.py
Normal file
129
common/lib/xmodule/xmodule/tests/test_annotatable_module.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Module annotatable tests"""
|
||||
|
||||
import unittest
|
||||
|
||||
from lxml import etree
|
||||
from mock import Mock
|
||||
|
||||
from xmodule.annotatable_module import AnnotatableModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from . import test_system
|
||||
|
||||
class AnnotatableModuleTestCase(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
|
||||
sample_xml = '''
|
||||
<annotatable display_name="Iliad">
|
||||
<instructions>Read the text.</instructions>
|
||||
<p>
|
||||
<annotation body="first">Sing</annotation>,
|
||||
<annotation title="goddess" body="second">O goddess</annotation>,
|
||||
<annotation title="anger" body="third" highlight="blue">the anger of Achilles son of Peleus</annotation>,
|
||||
that brought <i>countless</i> ills upon the Achaeans. Many a brave soul did it send
|
||||
hurrying down to Hades, and many a hero did it yield a prey to dogs and
|
||||
<div style="font-weight:bold"><annotation body="fourth" problem="4">vultures</annotation>, for so were the counsels
|
||||
of Jove fulfilled from the day on which the son of Atreus, king of men, and great
|
||||
Achilles, first fell out with one another.</div>
|
||||
</p>
|
||||
<annotation title="footnote" body="the end">The Iliad of Homer by Samuel Butler</annotation>
|
||||
</annotatable>
|
||||
'''
|
||||
definition = { 'data': sample_xml }
|
||||
descriptor = Mock()
|
||||
instance_state = None
|
||||
shared_state = None
|
||||
|
||||
def setUp(self):
|
||||
self.annotatable = AnnotatableModule(test_system(), self.location, self.definition, self.descriptor, self.instance_state, self.shared_state)
|
||||
|
||||
def test_annotation_data_attr(self):
|
||||
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
|
||||
|
||||
expected_attr = {
|
||||
'data-comment-body': {'value': 'foo', '_delete': 'body' },
|
||||
'data-comment-title': {'value': 'bar', '_delete': 'title'},
|
||||
'data-problem-id': {'value': '0', '_delete': 'problem'}
|
||||
}
|
||||
|
||||
actual_attr = self.annotatable._get_annotation_data_attr(0, el)
|
||||
|
||||
self.assertTrue(type(actual_attr) is dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_annotation_class_attr_default(self):
|
||||
xml = '<annotation title="x" body="y" problem="0">test</annotation>'
|
||||
el = etree.fromstring(xml)
|
||||
|
||||
expected_attr = { 'class': { 'value': 'annotatable-span highlight' } }
|
||||
actual_attr = self.annotatable._get_annotation_class_attr(0, el)
|
||||
|
||||
self.assertTrue(type(actual_attr) is dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_annotation_class_attr_with_valid_highlight(self):
|
||||
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
|
||||
|
||||
for color in self.annotatable.highlight_colors:
|
||||
el = etree.fromstring(xml.format(highlight=color))
|
||||
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
|
||||
|
||||
expected_attr = { 'class': {
|
||||
'value': value,
|
||||
'_delete': 'highlight' }
|
||||
}
|
||||
actual_attr = self.annotatable._get_annotation_class_attr(0, el)
|
||||
|
||||
self.assertTrue(type(actual_attr) is dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_annotation_class_attr_with_invalid_highlight(self):
|
||||
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
|
||||
|
||||
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
|
||||
el = etree.fromstring(xml.format(highlight=invalid_color))
|
||||
expected_attr = { 'class': {
|
||||
'value': 'annotatable-span highlight',
|
||||
'_delete': 'highlight' }
|
||||
}
|
||||
actual_attr = self.annotatable._get_annotation_class_attr(0, el)
|
||||
|
||||
self.assertTrue(type(actual_attr) is dict)
|
||||
self.assertDictEqual(expected_attr, actual_attr)
|
||||
|
||||
def test_render_annotation(self):
|
||||
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
|
||||
expected_el = etree.fromstring(expected_html)
|
||||
|
||||
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
|
||||
self.annotatable._render_annotation(0, actual_el)
|
||||
|
||||
self.assertEqual(expected_el.tag, actual_el.tag)
|
||||
self.assertEqual(expected_el.text, actual_el.text)
|
||||
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
|
||||
|
||||
def test_render_content(self):
|
||||
content = self.annotatable._render_content()
|
||||
el = etree.fromstring(content)
|
||||
|
||||
self.assertEqual('div', el.tag, 'root tag is a div')
|
||||
|
||||
expected_num_annotations = 5
|
||||
actual_num_annotations = el.xpath('count(//span[contains(@class,"annotatable-span")])')
|
||||
self.assertEqual(expected_num_annotations, actual_num_annotations, 'check number of annotations')
|
||||
|
||||
def test_get_html(self):
|
||||
context = self.annotatable.get_html()
|
||||
for key in ['display_name', 'element_id', 'content_html', 'instructions_html']:
|
||||
self.assertIn(key, context)
|
||||
|
||||
def test_extract_instructions(self):
|
||||
xmltree = etree.fromstring(self.sample_xml)
|
||||
|
||||
expected_xml = u"<div>Read the text.</div>"
|
||||
actual_xml = self.annotatable._extract_instructions(xmltree)
|
||||
self.assertIsNotNone(actual_xml)
|
||||
self.assertEqual(expected_xml.strip(), actual_xml.strip())
|
||||
|
||||
xmltree = etree.fromstring('<annotatable>foo</annotatable>')
|
||||
actual = self.annotatable._extract_instructions(xmltree)
|
||||
self.assertIsNone(actual)
|
||||
@@ -1,9 +1,12 @@
|
||||
import datetime
|
||||
import json
|
||||
from mock import Mock
|
||||
from mock import Mock, MagicMock, patch
|
||||
from pprint import pprint
|
||||
import unittest
|
||||
import random
|
||||
|
||||
import xmodule
|
||||
import capa
|
||||
from xmodule.capa_module import CapaModule
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
@@ -33,6 +36,18 @@ class CapaFactory(object):
|
||||
CapaFactory.num += 1
|
||||
return CapaFactory.num
|
||||
|
||||
@staticmethod
|
||||
def input_key():
|
||||
""" Return the input key to use when passing GET parameters """
|
||||
return ("input_" + CapaFactory.answer_key())
|
||||
|
||||
@staticmethod
|
||||
def answer_key():
|
||||
""" Return the key stored in the capa problem answer dict """
|
||||
return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
|
||||
'SampleProblem%d' % CapaFactory.num]) +
|
||||
"_2_1")
|
||||
|
||||
@staticmethod
|
||||
def create(graceperiod=None,
|
||||
due=None,
|
||||
@@ -59,11 +74,10 @@ class CapaFactory(object):
|
||||
module.
|
||||
|
||||
attempts: also added to instance state. Will be converted to an int.
|
||||
correct: if True, the problem will be initialized to be answered correctly.
|
||||
"""
|
||||
definition = {'data': CapaFactory.sample_problem_xml, }
|
||||
location = Location(["i4x", "edX", "capa_test", "problem",
|
||||
"SampleProblem{0}".format(CapaFactory.next_num())])
|
||||
"SampleProblem%d" % CapaFactory.next_num()])
|
||||
metadata = {}
|
||||
if graceperiod is not None:
|
||||
metadata['graceperiod'] = graceperiod
|
||||
@@ -89,19 +103,14 @@ class CapaFactory(object):
|
||||
# since everything else is a string.
|
||||
instance_state_dict['attempts'] = int(attempts)
|
||||
|
||||
if correct:
|
||||
# TODO: make this actually set an answer of 3.14, and mark it correct
|
||||
#instance_state_dict['student_answers'] = {}
|
||||
#instance_state_dict['correct_map'] = {}
|
||||
pass
|
||||
|
||||
|
||||
if len(instance_state_dict) > 0:
|
||||
instance_state = json.dumps(instance_state_dict)
|
||||
else:
|
||||
instance_state = None
|
||||
|
||||
module = CapaModule(test_system, location,
|
||||
system = test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
module = CapaModule(system, location,
|
||||
definition, descriptor,
|
||||
instance_state, None, metadata=metadata)
|
||||
|
||||
@@ -135,6 +144,8 @@ class CapaModuleTest(unittest.TestCase):
|
||||
"Factory should be creating unique names for each problem")
|
||||
|
||||
|
||||
|
||||
|
||||
def test_correct(self):
|
||||
"""
|
||||
Check that the factory creates correct and incorrect problems properly.
|
||||
@@ -178,6 +189,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
max_attempts="1",
|
||||
attempts="0",
|
||||
due=self.yesterday_str)
|
||||
|
||||
self.assertTrue(after_due_date.answer_available())
|
||||
|
||||
|
||||
@@ -282,3 +294,578 @@ class CapaModuleTest(unittest.TestCase):
|
||||
due=self.yesterday_str,
|
||||
graceperiod=self.two_day_delta_str)
|
||||
self.assertTrue(still_in_grace.answer_available())
|
||||
|
||||
|
||||
def test_closed(self):
|
||||
|
||||
# Attempts < Max attempts --> NOT closed
|
||||
module = CapaFactory.create(max_attempts="1", attempts="0")
|
||||
self.assertFalse(module.closed())
|
||||
|
||||
# Attempts < Max attempts --> NOT closed
|
||||
module = CapaFactory.create(max_attempts="2", attempts="1")
|
||||
self.assertFalse(module.closed())
|
||||
|
||||
# Attempts = Max attempts --> closed
|
||||
module = CapaFactory.create(max_attempts="1", attempts="1")
|
||||
self.assertTrue(module.closed())
|
||||
|
||||
# Attempts > Max attempts --> closed
|
||||
module = CapaFactory.create(max_attempts="1", attempts="2")
|
||||
self.assertTrue(module.closed())
|
||||
|
||||
# Max attempts = 0 --> closed
|
||||
module = CapaFactory.create(max_attempts="0", attempts="2")
|
||||
self.assertTrue(module.closed())
|
||||
|
||||
# Past due --> closed
|
||||
module = CapaFactory.create(max_attempts="1", attempts="0",
|
||||
due=self.yesterday_str)
|
||||
self.assertTrue(module.closed())
|
||||
|
||||
|
||||
def test_parse_get_params(self):
|
||||
|
||||
# Valid GET param dict
|
||||
valid_get_dict = {'input_1': 'test',
|
||||
'input_1_2': 'test',
|
||||
'input_1_2_3': 'test',
|
||||
'input_[]_3': 'test',
|
||||
'input_4': None,
|
||||
'input_5': [],
|
||||
'input_6': 5}
|
||||
|
||||
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||
|
||||
# Expect that we get a dict with "input" stripped from key names
|
||||
# and that we get the same values back
|
||||
for key in result.keys():
|
||||
original_key = "input_" + key
|
||||
self.assertTrue(original_key in valid_get_dict,
|
||||
"Output dict should have key %s" % original_key)
|
||||
self.assertEqual(valid_get_dict[original_key], result[key])
|
||||
|
||||
|
||||
# Valid GET param dict with list keys
|
||||
valid_get_dict = {'input_2[]': ['test1', 'test2']}
|
||||
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||
self.assertTrue('2' in result)
|
||||
self.assertEqual(valid_get_dict['input_2[]'], result['2'])
|
||||
|
||||
# If we use [] at the end of a key name, we should always
|
||||
# get a list, even if there's just one value
|
||||
valid_get_dict = {'input_1[]': 'test'}
|
||||
result = CapaModule.make_dict_of_responses(valid_get_dict)
|
||||
self.assertEqual(result['1'], ['test'])
|
||||
|
||||
|
||||
# If we have no underscores in the name, then the key is invalid
|
||||
invalid_get_dict = {'input': 'test'}
|
||||
with self.assertRaises(ValueError):
|
||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
||||
|
||||
|
||||
# Two equivalent names (one list, one non-list)
|
||||
# One of the values would overwrite the other, so detect this
|
||||
# and raise an exception
|
||||
invalid_get_dict = {'input_1[]': 'test 1',
|
||||
'input_1': 'test 2' }
|
||||
with self.assertRaises(ValueError):
|
||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
||||
|
||||
def test_check_problem_correct(self):
|
||||
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Simulate that all answers are marked correct, no matter
|
||||
# what the input is, by patching CorrectMap.is_correct()
|
||||
# Also simulate rendering the HTML
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
|
||||
patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
|
||||
mock_is_correct.return_value = True
|
||||
mock_html.return_value = "Test HTML"
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
|
||||
# Expect that we get the (mocked) HTML
|
||||
self.assertEqual(result['contents'], 'Test HTML')
|
||||
|
||||
# Expect that the number of attempts is incremented by 1
|
||||
self.assertEqual(module.attempts, 2)
|
||||
|
||||
|
||||
def test_check_problem_incorrect(self):
|
||||
|
||||
module = CapaFactory.create(attempts=0)
|
||||
|
||||
# Simulate marking the input incorrect
|
||||
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
||||
mock_is_correct.return_value = False
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '0' }
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
self.assertEqual(result['success'], 'incorrect')
|
||||
|
||||
# Expect that the number of attempts is incremented by 1
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_check_problem_closed(self):
|
||||
module = CapaFactory.create(attempts=3)
|
||||
|
||||
# Problem closed -- cannot submit
|
||||
# Simulate that CapaModule.closed() always returns True
|
||||
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
|
||||
mock_closed.return_value = True
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 3)
|
||||
|
||||
|
||||
def test_check_problem_resubmitted_with_randomize(self):
|
||||
# Randomize turned on
|
||||
module = CapaFactory.create(rerandomize='always', attempts=0)
|
||||
|
||||
# Simulate that the problem is completed
|
||||
module.lcp.done = True
|
||||
|
||||
# Expect that we cannot submit
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
self.assertEqual(module.attempts, 0)
|
||||
|
||||
|
||||
def test_check_problem_resubmitted_no_randomize(self):
|
||||
# Randomize turned off
|
||||
module = CapaFactory.create(rerandomize='never', attempts=0)
|
||||
|
||||
# Simulate that the problem is completed
|
||||
module.lcp.done = True
|
||||
|
||||
# Expect that we can submit successfully
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
self.assertEqual(result['success'], 'correct')
|
||||
|
||||
# Expect that number of attempts IS incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_check_problem_queued(self):
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Simulate that the problem is queued
|
||||
with patch('capa.capa_problem.LoncapaProblem.is_queued') \
|
||||
as mock_is_queued,\
|
||||
patch('capa.capa_problem.LoncapaProblem.get_recentmost_queuetime') \
|
||||
as mock_get_queuetime:
|
||||
|
||||
mock_is_queued.return_value = True
|
||||
mock_get_queuetime.return_value = datetime.datetime.now()
|
||||
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
self.assertTrue('You must wait' in result['success'])
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_check_problem_student_input_error(self):
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Simulate a student input exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error')
|
||||
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
self.assertTrue('test error' in result['success'])
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_reset_problem(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Mock the module's capa problem
|
||||
# to simulate that the problem is done
|
||||
mock_problem = MagicMock(capa.capa_problem.LoncapaProblem)
|
||||
mock_problem.done = True
|
||||
module.lcp = mock_problem
|
||||
|
||||
# Stub out HTML rendering
|
||||
with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
|
||||
mock_html.return_value = "<div>Test HTML</div>"
|
||||
|
||||
# Reset the problem
|
||||
get_request_dict = {}
|
||||
result = module.reset_problem(get_request_dict)
|
||||
|
||||
# Expect that the request was successful
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
|
||||
# Expect that the problem HTML is retrieved
|
||||
self.assertTrue('html' in result)
|
||||
self.assertEqual(result['html'], "<div>Test HTML</div>")
|
||||
|
||||
# Expect that the problem was reset
|
||||
mock_problem.do_reset.assert_called_once_with()
|
||||
|
||||
|
||||
def test_reset_problem_closed(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Simulate that the problem is closed
|
||||
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
|
||||
mock_closed.return_value = True
|
||||
|
||||
# Try to reset the problem
|
||||
get_request_dict = {}
|
||||
result = module.reset_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem was NOT reset
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_reset_problem_not_done(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Simulate that the problem is NOT done
|
||||
module.lcp.done = False
|
||||
|
||||
# Try to reset the problem
|
||||
get_request_dict = {}
|
||||
result = module.reset_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem was NOT reset
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_save_problem(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Simulate that the problem is not done (not attempted or reset)
|
||||
module.lcp.done = False
|
||||
|
||||
# Save the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that answers are saved to the problem
|
||||
expected_answers = { CapaFactory.answer_key(): '3.14' }
|
||||
self.assertEqual(module.lcp.student_answers, expected_answers)
|
||||
|
||||
# Expect that the result is success
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
|
||||
|
||||
def test_save_problem_closed(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Simulate that the problem is NOT done (not attempted or reset)
|
||||
module.lcp.done = False
|
||||
|
||||
# Simulate that the problem is closed
|
||||
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
|
||||
mock_closed.return_value = True
|
||||
|
||||
# Try to save the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that the result is failure
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_save_problem_submitted_with_randomize(self):
|
||||
module = CapaFactory.create(rerandomize='always')
|
||||
|
||||
# Simulate that the problem is completed
|
||||
module.lcp.done = True
|
||||
|
||||
# Try to save
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we cannot save
|
||||
self.assertTrue('success' in result and not result['success'])
|
||||
|
||||
|
||||
def test_save_problem_submitted_no_randomize(self):
|
||||
module = CapaFactory.create(rerandomize='never')
|
||||
|
||||
# Simulate that the problem is completed
|
||||
module.lcp.done = True
|
||||
|
||||
# Try to save
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14' }
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we succeed
|
||||
self.assertTrue('success' in result and result['success'])
|
||||
|
||||
def test_check_button_name(self):
|
||||
|
||||
# If last attempt, button name changes to "Final Check"
|
||||
# Just in case, we also check what happens if we have
|
||||
# more attempts than allowed.
|
||||
attempts = random.randint(1, 10)
|
||||
module = CapaFactory.create(attempts=attempts-1, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Final Check")
|
||||
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Final Check")
|
||||
|
||||
module = CapaFactory.create(attempts=attempts + 1, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Final Check")
|
||||
|
||||
# Otherwise, button name is "Check"
|
||||
module = CapaFactory.create(attempts=attempts-2, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
module = CapaFactory.create(attempts=attempts-3, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
# If no limit on attempts, then always show "Check"
|
||||
module = CapaFactory.create(attempts=attempts-3)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
module = CapaFactory.create(attempts=0)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
def test_should_show_check_button(self):
|
||||
|
||||
attempts = random.randint(1,10)
|
||||
|
||||
# If we're after the deadline, do NOT show check button
|
||||
module = CapaFactory.create(due=self.yesterday_str)
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
# If user is out of attempts, do NOT show the check button
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
# If survey question (max_attempts = 0), do NOT show the check button
|
||||
module = CapaFactory.create(max_attempts=0)
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
# If user submitted a problem but hasn't reset,
|
||||
# do NOT show the check button
|
||||
# Note: we can only reset when rerandomize="always"
|
||||
module = CapaFactory.create(rerandomize="always")
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_check_button())
|
||||
|
||||
# Otherwise, DO show the check button
|
||||
module = CapaFactory.create()
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
# If the user has submitted the problem
|
||||
# and we do NOT have a reset button, then we can show the check button
|
||||
# Setting rerandomize to "never" ensures that the reset button
|
||||
# is not shown
|
||||
module = CapaFactory.create(rerandomize="never")
|
||||
module.lcp.done = True
|
||||
self.assertTrue(module.should_show_check_button())
|
||||
|
||||
|
||||
def test_should_show_reset_button(self):
|
||||
|
||||
attempts = random.randint(1,10)
|
||||
|
||||
# If we're after the deadline, do NOT show the reset button
|
||||
module = CapaFactory.create(due=self.yesterday_str)
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If the user is out of attempts, do NOT show the reset button
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If we're NOT randomizing, then do NOT show the reset button
|
||||
module = CapaFactory.create(rerandomize="never")
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# If the user hasn't submitted an answer yet,
|
||||
# then do NOT show the reset button
|
||||
module = CapaFactory.create()
|
||||
module.lcp.done = False
|
||||
self.assertFalse(module.should_show_reset_button())
|
||||
|
||||
# Otherwise, DO show the reset button
|
||||
module = CapaFactory.create()
|
||||
module.lcp.done = True
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
# If survey question for capa (max_attempts = 0),
|
||||
# DO show the reset button
|
||||
module = CapaFactory.create(max_attempts=0)
|
||||
module.lcp.done = True
|
||||
self.assertTrue(module.should_show_reset_button())
|
||||
|
||||
|
||||
def test_should_show_save_button(self):
|
||||
|
||||
attempts = random.randint(1,10)
|
||||
|
||||
# If we're after the deadline, do NOT show the save button
|
||||
module = CapaFactory.create(due=self.yesterday_str)
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# If the user is out of attempts, do NOT show the save button
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# If user submitted a problem but hasn't reset, do NOT show the save button
|
||||
module = CapaFactory.create(rerandomize="always")
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# Otherwise, DO show the save button
|
||||
module = CapaFactory.create()
|
||||
module.lcp.done = False
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
# If we're not randomizing, then we can re-save
|
||||
module = CapaFactory.create(rerandomize="never")
|
||||
module.lcp.done = True
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
# If survey question for capa (max_attempts = 0),
|
||||
# DO show the save button
|
||||
module = CapaFactory.create(max_attempts=0)
|
||||
module.lcp.done = False
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
def test_should_show_save_button_force_save_button(self):
|
||||
# If we're after the deadline, do NOT show the save button
|
||||
# even though we're forcing a save
|
||||
module = CapaFactory.create(due=self.yesterday_str,
|
||||
force_save_button="true")
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# If the user is out of attempts, do NOT show the save button
|
||||
attempts = random.randint(1,10)
|
||||
module = CapaFactory.create(attempts=attempts,
|
||||
max_attempts=attempts,
|
||||
force_save_button="true")
|
||||
module.lcp.done = True
|
||||
self.assertFalse(module.should_show_save_button())
|
||||
|
||||
# Otherwise, if we force the save button,
|
||||
# then show it even if we would ordinarily
|
||||
# require a reset first
|
||||
module = CapaFactory.create(force_save_button="true",
|
||||
rerandomize="always")
|
||||
module.lcp.done = True
|
||||
self.assertTrue(module.should_show_save_button())
|
||||
|
||||
def test_no_max_attempts(self):
|
||||
module = CapaFactory.create(max_attempts='')
|
||||
html = module.get_problem_html()
|
||||
# assert that we got here without exploding
|
||||
|
||||
|
||||
def test_get_problem_html(self):
|
||||
module = CapaFactory.create()
|
||||
|
||||
# We've tested the show/hide button logic in other tests,
|
||||
# so here we hard-wire the values
|
||||
show_check_button = bool(random.randint(0,1) % 2)
|
||||
show_reset_button = bool(random.randint(0,1) % 2)
|
||||
show_save_button = bool(random.randint(0,1) % 2)
|
||||
|
||||
module.should_show_check_button = Mock(return_value=show_check_button)
|
||||
module.should_show_reset_button = Mock(return_value=show_reset_button)
|
||||
module.should_show_save_button = Mock(return_value=show_save_button)
|
||||
|
||||
# Mock the system rendering function
|
||||
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
|
||||
# Patch the capa problem's HTML rendering
|
||||
with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
|
||||
mock_html.return_value = "<div>Test Problem HTML</div>"
|
||||
|
||||
# Render the problem HTML
|
||||
html = module.get_problem_html(encapsulate=False)
|
||||
|
||||
# Also render the problem encapsulated in a <div>
|
||||
html_encapsulated = module.get_problem_html(encapsulate=True)
|
||||
|
||||
# Expect that we get the rendered template back
|
||||
self.assertEqual(html, "<div>Test Template HTML</div>")
|
||||
|
||||
# Check the rendering context
|
||||
render_args,_ = module.system.render_template.call_args
|
||||
self.assertEqual(len(render_args), 2)
|
||||
|
||||
template_name = render_args[0]
|
||||
self.assertEqual(template_name, "problem.html")
|
||||
|
||||
context = render_args[1]
|
||||
self.assertEqual(context['problem']['html'], "<div>Test Problem HTML</div>")
|
||||
self.assertEqual(bool(context['check_button']), show_check_button)
|
||||
self.assertEqual(bool(context['reset_button']), show_reset_button)
|
||||
self.assertEqual(bool(context['save_button']), show_save_button)
|
||||
|
||||
# Assert that the encapsulated html contains the original html
|
||||
self.assertTrue(html in html_encapsulated)
|
||||
|
||||
|
||||
def test_get_problem_html_error(self):
|
||||
"""
|
||||
In production, when an error occurs with the problem HTML
|
||||
rendering, a "dummy" problem is created with an error
|
||||
message to display to the user.
|
||||
"""
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Save the original problem so we can compare it later
|
||||
original_problem = module.lcp
|
||||
|
||||
# Simulate throwing an exception when the capa problem
|
||||
# is asked to render itself as HTML
|
||||
module.lcp.get_html = Mock(side_effect=Exception("Test"))
|
||||
|
||||
# Stub out the test_system rendering function
|
||||
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
|
||||
# Turn off DEBUG
|
||||
module.system.DEBUG = False
|
||||
|
||||
# Try to render the module with DEBUG turned off
|
||||
html = module.get_problem_html()
|
||||
|
||||
# Check the rendering context
|
||||
render_args,_ = module.system.render_template.call_args
|
||||
context = render_args[1]
|
||||
self.assertTrue("error" in context['problem']['html'])
|
||||
|
||||
# Expect that the module has created a new dummy problem with the error
|
||||
self.assertNotEqual(original_problem, module.lcp)
|
||||
|
||||
@@ -54,7 +54,8 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.openendedchild = OpenEndedChild(test_system, self.location,
|
||||
self.test_system = test_system()
|
||||
self.openendedchild = OpenEndedChild(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
|
||||
@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
|
||||
|
||||
def test_latest_post_assessment_empty(self):
|
||||
answer = self.openendedchild.latest_post_assessment(test_system)
|
||||
answer = self.openendedchild.latest_post_assessment(self.test_system)
|
||||
self.assertEqual(answer, "")
|
||||
|
||||
|
||||
@@ -106,7 +107,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
post_assessment = "Post assessment"
|
||||
self.openendedchild.record_latest_post_assessment(post_assessment)
|
||||
self.assertEqual(post_assessment,
|
||||
self.openendedchild.latest_post_assessment(test_system))
|
||||
self.openendedchild.latest_post_assessment(self.test_system))
|
||||
|
||||
def test_get_score(self):
|
||||
new_answer = "New Answer"
|
||||
@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
|
||||
|
||||
def test_reset(self):
|
||||
self.openendedchild.reset(test_system)
|
||||
self.openendedchild.reset(self.test_system)
|
||||
state = json.loads(self.openendedchild.get_instance_state())
|
||||
self.assertEqual(state['state'], OpenEndedChild.INITIAL)
|
||||
|
||||
@@ -182,11 +183,13 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
test_system.location = self.location
|
||||
self.test_system = test_system()
|
||||
|
||||
self.test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
|
||||
test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
|
||||
self.openendedmodule = OpenEndedModule(test_system, self.location,
|
||||
self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
|
||||
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
def test_message_post(self):
|
||||
@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'grader_id': '1',
|
||||
'score': 3}
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
|
||||
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = {
|
||||
'feedback': get['feedback'],
|
||||
@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'student_info': json.dumps(student_info)
|
||||
}
|
||||
|
||||
result = self.openendedmodule.message_post(get, test_system)
|
||||
result = self.openendedmodule.message_post(get, self.test_system)
|
||||
self.assertTrue(result['success'])
|
||||
# make sure it's actually sending something we want to the queue
|
||||
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
|
||||
@@ -216,7 +219,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
def test_send_to_grader(self):
|
||||
submission = "This is a student submission"
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
student_info = {'anonymous_student_id': test_system.anonymous_student_id,
|
||||
student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = self.openendedmodule.payload.copy()
|
||||
contents.update({
|
||||
@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
'student_response': submission,
|
||||
'max_score': self.max_score
|
||||
})
|
||||
result = self.openendedmodule.send_to_grader(submission, test_system)
|
||||
result = self.openendedmodule.send_to_grader(submission, self.test_system)
|
||||
self.assertTrue(result)
|
||||
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
|
||||
|
||||
@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
}
|
||||
get = {'queuekey': "abcd",
|
||||
'xqueue_body': score_msg}
|
||||
self.openendedmodule.update_score(get, test_system)
|
||||
self.openendedmodule.update_score(get, self.test_system)
|
||||
|
||||
def update_score_single(self):
|
||||
self.openendedmodule.new_history_entry("New Entry")
|
||||
@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
}
|
||||
get = {'queuekey': "abcd",
|
||||
'xqueue_body': json.dumps(score_msg)}
|
||||
self.openendedmodule.update_score(get, test_system)
|
||||
self.openendedmodule.update_score(get, self.test_system)
|
||||
|
||||
def test_latest_post_assessment(self):
|
||||
self.update_score_single()
|
||||
assessment = self.openendedmodule.latest_post_assessment(test_system)
|
||||
assessment = self.openendedmodule.latest_post_assessment(self.test_system)
|
||||
self.assertFalse(assessment == '')
|
||||
# check for errors
|
||||
self.assertFalse('errors' in assessment)
|
||||
@@ -336,7 +339,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata)
|
||||
self.test_system = test_system()
|
||||
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
|
||||
self.location,
|
||||
self.definition,
|
||||
self.descriptor,
|
||||
static_data = self.static_data,
|
||||
metadata=self.metadata)
|
||||
|
||||
def test_get_tag_name(self):
|
||||
name = self.combinedoe.get_tag_name("<t>Tag</t>")
|
||||
|
||||
@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
'''Get a dummy system'''
|
||||
return DummySystem(load_error_modules)
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
|
||||
def get_course(self, name):
|
||||
"""Get a test course by directory name. If there's more than one, error."""
|
||||
print "Importing {0}".format(name)
|
||||
@@ -85,14 +88,14 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
location = descriptor.location
|
||||
instance_state = instance_states.get(location.category, None)
|
||||
print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
|
||||
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
|
||||
return descriptor.xmodule_constructor(self.test_system)(instance_state, shared_state)
|
||||
|
||||
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
|
||||
|
||||
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
|
||||
return text
|
||||
test_system.replace_urls = replace_urls
|
||||
test_system.get_module = inner_get_module
|
||||
self.test_system.replace_urls = replace_urls
|
||||
self.test_system.get_module = inner_get_module
|
||||
|
||||
module = inner_get_module(location)
|
||||
print "module: ", module
|
||||
|
||||
@@ -19,9 +19,14 @@ class ContentTest(unittest.TestCase):
|
||||
|
||||
content = StaticContent('loc', 'name', 'content_type', 'data')
|
||||
self.assertIsNone(content.thumbnail_location)
|
||||
def test_generate_thumbnail_nonimage(self):
|
||||
def test_generate_thumbnail_image(self):
|
||||
contentStore = ContentStore()
|
||||
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters.jpg'), None)
|
||||
content = Content(Location(u'c4x', u'mitX', u'800', u'asset', u'monsters__.jpg'), None)
|
||||
(thumbnail_content, thumbnail_file_location) = contentStore.generate_thumbnail(content)
|
||||
self.assertIsNone(thumbnail_content)
|
||||
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg'), thumbnail_file_location)
|
||||
self.assertEqual(Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters__.jpg'), thumbnail_file_location)
|
||||
def test_compute_location(self):
|
||||
# We had a bug that __ got converted into a single _. Make sure that substitution of INVALID_CHARS (like space)
|
||||
# still happen.
|
||||
asset_location = StaticContent.compute_location('mitX', '400', 'subs__1eo_jXvZnE .srt.sjson')
|
||||
self.assertEqual(Location(u'c4x', u'mitX', u'400', u'asset', u'subs__1eo_jXvZnE_.srt.sjson', None), asset_location)
|
||||
|
||||
@@ -53,13 +53,13 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
'skip_basic_checks' : False,
|
||||
}
|
||||
|
||||
self.module = SelfAssessmentModule(test_system, self.location,
|
||||
self.module = SelfAssessmentModule(test_system(), self.location,
|
||||
self.definition, self.descriptor,
|
||||
static_data,
|
||||
state, metadata=self.metadata)
|
||||
|
||||
def test_get_html(self):
|
||||
html = self.module.get_html(test_system)
|
||||
html = self.module.get_html(self.module.system)
|
||||
self.assertTrue("This is sample prompt text" in html)
|
||||
|
||||
def test_self_assessment_flow(self):
|
||||
@@ -82,10 +82,11 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(self.module.get_score()['score'], 0)
|
||||
|
||||
self.module.save_answer({'student_answer': "I am an answer"}, test_system)
|
||||
self.module.save_answer({'student_answer': "I am an answer"},
|
||||
self.module.system)
|
||||
self.assertEqual(self.module.state, self.module.ASSESSING)
|
||||
|
||||
self.module.save_assessment(mock_query_dict, test_system)
|
||||
self.module.save_assessment(mock_query_dict, self.module.system)
|
||||
self.assertEqual(self.module.state, self.module.DONE)
|
||||
|
||||
|
||||
@@ -94,7 +95,8 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
self.assertEqual(self.module.state, self.module.INITIAL)
|
||||
|
||||
# if we now assess as right, skip the REQUEST_HINT state
|
||||
self.module.save_answer({'student_answer': 'answer 4'}, test_system)
|
||||
self.module.save_answer({'student_answer': 'answer 4'},
|
||||
self.module.system)
|
||||
responses['assessment'] = '1'
|
||||
self.module.save_assessment(mock_query_dict, test_system)
|
||||
self.module.save_assessment(mock_query_dict, self.module.system)
|
||||
self.assertEqual(self.module.state, self.module.DONE)
|
||||
|
||||
@@ -379,7 +379,11 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
if attr not in self.metadata_to_strip and attr not in self.metadata_to_export_to_policy:
|
||||
val = val_for_xml(attr)
|
||||
#logging.debug('location.category = {0}, attr = {1}'.format(self.location.category, attr))
|
||||
xml_object.set(attr, val)
|
||||
try:
|
||||
xml_object.set(attr, val)
|
||||
except Exception, e:
|
||||
logging.exception('Failed to serialize metadata attribute {0} with value {1}. This could mean data loss!!! Exception: {2}'.format(attr, val, e))
|
||||
pass
|
||||
|
||||
if self.export_to_file():
|
||||
# Write the definition to a file
|
||||
|
||||
Reference in New Issue
Block a user