Extends the common capa response types (string, numeric, multiple choice, checkbox, dropdown) with feedback and hint capabilities. "Feedback" refers to feedback shown to the student when they check the problem, looking at their specific answer. "Hints" refers to a Hint button in LMS which the student can click at any time to see hints for that problem. The implementation extends the markdown syntax to include feedback and hints. There are new Feedback-and-Hint specific templates in Studio when the author clicks to add a new problem.
244 lines
8.6 KiB
Python
244 lines
8.6 KiB
Python
"""Implements basics of Capa, including class CapaModule."""
|
|
import json
|
|
import logging
|
|
import sys
|
|
from lxml import etree
|
|
|
|
from pkg_resources import resource_string
|
|
|
|
import dogstats_wrapper as dog_stats_api
|
|
from .capa_base import CapaMixin, CapaFields, ComplexEncoder
|
|
from capa import responsetypes
|
|
from .progress import Progress
|
|
from xmodule.x_module import XModule, module_attr, DEPRECATION_VSCOMPAT_EVENT
|
|
from xmodule.raw_module import RawDescriptor
|
|
from xmodule.exceptions import NotFoundError, ProcessingError
|
|
|
|
log = logging.getLogger("edx.courseware")
|
|
|
|
|
|
class CapaModule(CapaMixin, XModule):
|
|
"""
|
|
An XModule implementing LonCapa format problems, implemented by way of
|
|
capa.capa_problem.LoncapaProblem
|
|
|
|
CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
|
|
"""
|
|
icon_class = 'problem'
|
|
|
|
js = {
|
|
'coffee': [
|
|
resource_string(__name__, 'js/src/capa/display.coffee'),
|
|
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
|
],
|
|
'js': [
|
|
resource_string(__name__, 'js/src/collapsible.js'),
|
|
resource_string(__name__, 'js/src/capa/imageinput.js'),
|
|
resource_string(__name__, 'js/src/capa/schematic.js'),
|
|
]
|
|
}
|
|
|
|
js_module_name = "Problem"
|
|
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
Accepts the same arguments as xmodule.x_module:XModule.__init__
|
|
"""
|
|
super(CapaModule, self).__init__(*args, **kwargs)
|
|
|
|
def handle_ajax(self, dispatch, data):
|
|
"""
|
|
This is called by courseware.module_render, to handle an AJAX call.
|
|
|
|
`data` is request.POST.
|
|
|
|
Returns a json dictionary:
|
|
{ 'progress_changed' : True/False,
|
|
'progress' : 'none'/'in_progress'/'done',
|
|
<other request-specific values here > }
|
|
"""
|
|
handlers = {
|
|
'hint_button': self.hint_button,
|
|
'problem_get': self.get_problem,
|
|
'problem_check': self.check_problem,
|
|
'problem_reset': self.reset_problem,
|
|
'problem_save': self.save_problem,
|
|
'problem_show': self.get_answer,
|
|
'score_update': self.update_score,
|
|
'input_ajax': self.handle_input_ajax,
|
|
'ungraded_response': self.handle_ungraded_response
|
|
}
|
|
|
|
_ = self.runtime.service(self, "i18n").ugettext
|
|
|
|
generic_error_message = _(
|
|
"We're sorry, there was an error with processing your request. "
|
|
"Please try reloading your page and trying again."
|
|
)
|
|
|
|
not_found_error_message = _(
|
|
"The state of this problem has changed since you loaded this page. "
|
|
"Please refresh your page."
|
|
)
|
|
|
|
if dispatch not in handlers:
|
|
return 'Error: {} is not a known capa action'.format(dispatch)
|
|
|
|
before = self.get_progress()
|
|
|
|
try:
|
|
result = handlers[dispatch](data)
|
|
|
|
except NotFoundError as err:
|
|
_, _, traceback_obj = sys.exc_info() # pylint: disable=redefined-outer-name
|
|
raise ProcessingError(not_found_error_message), None, traceback_obj
|
|
|
|
except Exception as err:
|
|
_, _, traceback_obj = sys.exc_info() # pylint: disable=redefined-outer-name
|
|
raise ProcessingError(generic_error_message), None, traceback_obj
|
|
|
|
after = self.get_progress()
|
|
|
|
result.update({
|
|
'progress_changed': after != before,
|
|
'progress_status': Progress.to_js_status_str(after),
|
|
'progress_detail': Progress.to_js_detail_str(after),
|
|
})
|
|
|
|
return json.dumps(result, cls=ComplexEncoder)
|
|
|
|
|
|
class CapaDescriptor(CapaFields, RawDescriptor):
|
|
"""
|
|
Module implementing problems in the LON-CAPA format,
|
|
as implemented by capa.capa_problem
|
|
"""
|
|
INDEX_CONTENT_TYPE = 'CAPA'
|
|
|
|
module_class = CapaModule
|
|
|
|
has_score = True
|
|
template_dir_name = 'problem'
|
|
mako_template = "widgets/problem-edit.html"
|
|
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
|
|
js_module_name = "MarkdownEditingDescriptor"
|
|
css = {
|
|
'scss': [
|
|
resource_string(__name__, 'css/editor/edit.scss'),
|
|
resource_string(__name__, 'css/problem/edit.scss')
|
|
]
|
|
}
|
|
|
|
# The capa format specifies that what we call max_attempts in the code
|
|
# is the attribute `attempts`. This will do that conversion
|
|
metadata_translations = dict(RawDescriptor.metadata_translations)
|
|
metadata_translations['attempts'] = 'max_attempts'
|
|
|
|
@classmethod
|
|
def filter_templates(cls, template, course):
|
|
"""
|
|
Filter template that contains 'latex' from templates.
|
|
|
|
Show them only if use_latex_compiler is set to True in
|
|
course settings.
|
|
"""
|
|
return ('latex' not in template['template_id'] or course.use_latex_compiler)
|
|
|
|
def get_context(self):
|
|
_context = RawDescriptor.get_context(self)
|
|
_context.update({
|
|
'markdown': self.markdown,
|
|
'enable_markdown': self.markdown is not None,
|
|
'enable_latex_compiler': self.use_latex_compiler,
|
|
})
|
|
return _context
|
|
|
|
# VS[compat]
|
|
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
|
# edited in the cms
|
|
@classmethod
|
|
def backcompat_paths(cls, path):
|
|
dog_stats_api.increment(
|
|
DEPRECATION_VSCOMPAT_EVENT,
|
|
tags=["location:capa_descriptor_backcompat_paths"]
|
|
)
|
|
return [
|
|
'problems/' + path[8:],
|
|
path[8:],
|
|
]
|
|
|
|
@property
|
|
def non_editable_metadata_fields(self):
|
|
non_editable_fields = super(CapaDescriptor, self).non_editable_metadata_fields
|
|
non_editable_fields.extend([
|
|
CapaDescriptor.due,
|
|
CapaDescriptor.graceperiod,
|
|
CapaDescriptor.force_save_button,
|
|
CapaDescriptor.markdown,
|
|
CapaDescriptor.text_customization,
|
|
CapaDescriptor.use_latex_compiler,
|
|
])
|
|
return non_editable_fields
|
|
|
|
@property
|
|
def problem_types(self):
|
|
""" Low-level problem type introspection for content libraries filtering by problem type """
|
|
tree = etree.XML(self.data) # pylint: disable=no-member
|
|
registered_tags = responsetypes.registry.registered_tags()
|
|
return set([node.tag for node in tree.iter() if node.tag in registered_tags])
|
|
|
|
@property
|
|
def has_responsive_ui(self):
|
|
"""
|
|
Returns whether this module has support for responsive UI.
|
|
"""
|
|
return self.lcp.has_responsive_ui
|
|
|
|
def index_dictionary(self):
|
|
"""
|
|
Return dictionary prepared with module content and type for indexing.
|
|
"""
|
|
result = super(CapaDescriptor, self).index_dictionary()
|
|
if not result:
|
|
result = {}
|
|
index = {
|
|
'content_type': self.INDEX_CONTENT_TYPE,
|
|
'problem_types': list(self.problem_types),
|
|
"display_name": self.display_name
|
|
}
|
|
result.update(index)
|
|
return result
|
|
|
|
# Proxy to CapaModule for access to any of its attributes
|
|
answer_available = module_attr('answer_available')
|
|
check_button_name = module_attr('check_button_name')
|
|
check_button_checking_name = module_attr('check_button_checking_name')
|
|
check_problem = module_attr('check_problem')
|
|
choose_new_seed = module_attr('choose_new_seed')
|
|
closed = module_attr('closed')
|
|
get_answer = module_attr('get_answer')
|
|
get_problem = module_attr('get_problem')
|
|
get_problem_html = module_attr('get_problem_html')
|
|
get_state_for_lcp = module_attr('get_state_for_lcp')
|
|
handle_input_ajax = module_attr('handle_input_ajax')
|
|
hint_button = module_attr('hint_button')
|
|
handle_problem_html_error = module_attr('handle_problem_html_error')
|
|
handle_ungraded_response = module_attr('handle_ungraded_response')
|
|
is_attempted = module_attr('is_attempted')
|
|
is_correct = module_attr('is_correct')
|
|
is_past_due = module_attr('is_past_due')
|
|
is_submitted = module_attr('is_submitted')
|
|
lcp = module_attr('lcp')
|
|
make_dict_of_responses = module_attr('make_dict_of_responses')
|
|
new_lcp = module_attr('new_lcp')
|
|
publish_grade = module_attr('publish_grade')
|
|
rescore_problem = module_attr('rescore_problem')
|
|
reset_problem = module_attr('reset_problem')
|
|
save_problem = module_attr('save_problem')
|
|
set_state_from_lcp = module_attr('set_state_from_lcp')
|
|
should_show_check_button = module_attr('should_show_check_button')
|
|
should_show_reset_button = module_attr('should_show_reset_button')
|
|
should_show_save_button = module_attr('should_show_save_button')
|
|
update_score = module_attr('update_score')
|