adding in self assessment grader
This commit is contained in:
@@ -29,10 +29,10 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
|
||||
|
||||
def only_one(lst, default="", process=lambda x: x):
|
||||
"""
|
||||
If lst is empty, returns default
|
||||
If lst has a single element, applies process to that element and returns it
|
||||
Otherwise, raises an exeception
|
||||
"""
|
||||
If lst is empty, returns default
|
||||
If lst has a single element, applies process to that element and returns it
|
||||
Otherwise, raises an exeception
|
||||
"""
|
||||
if len(lst) == 0:
|
||||
return default
|
||||
elif len(lst) == 1:
|
||||
@@ -43,14 +43,14 @@ Otherwise, raises an exeception
|
||||
|
||||
def parse_timedelta(time_str):
|
||||
"""
|
||||
time_str: A string with the following components:
|
||||
<D> day[s] (optional)
|
||||
<H> hour[s] (optional)
|
||||
<M> minute[s] (optional)
|
||||
<S> second[s] (optional)
|
||||
time_str: A string with the following components:
|
||||
<D> day[s] (optional)
|
||||
<H> hour[s] (optional)
|
||||
<M> minute[s] (optional)
|
||||
<S> second[s] (optional)
|
||||
|
||||
Returns a datetime.timedelta parsed from the string
|
||||
"""
|
||||
Returns a datetime.timedelta parsed from the string
|
||||
"""
|
||||
parts = TIMEDELTA_REGEX.match(time_str)
|
||||
if not parts:
|
||||
return
|
||||
@@ -71,9 +71,9 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
class CapaModule(XModule):
|
||||
'''
|
||||
An XModule implementing LonCapa format problems, implemented by way of
|
||||
capa.capa_problem.LoncapaProblem
|
||||
'''
|
||||
An XModule implementing LonCapa format problems, implemented by way of
|
||||
capa.capa_problem.LoncapaProblem
|
||||
'''
|
||||
icon_class = 'problem'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
|
||||
@@ -175,9 +175,9 @@ capa.capa_problem.LoncapaProblem
|
||||
@property
|
||||
def rerandomize(self):
|
||||
"""
|
||||
Property accessor that returns self.metadata['rerandomize'] in a
|
||||
canonical form
|
||||
"""
|
||||
Property accessor that returns self.metadata['rerandomize'] in a
|
||||
canonical form
|
||||
"""
|
||||
rerandomize = self.metadata.get('rerandomize', 'always')
|
||||
if rerandomize in ("", "always", "true"):
|
||||
return "always"
|
||||
@@ -203,7 +203,7 @@ canonical form
|
||||
|
||||
def get_progress(self):
|
||||
''' For now, just return score / max_score
|
||||
'''
|
||||
'''
|
||||
d = self.get_score()
|
||||
score = d['score']
|
||||
total = d['total']
|
||||
@@ -224,7 +224,7 @@ canonical form
|
||||
|
||||
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.'''
|
||||
as necessary based on the problem config and state.'''
|
||||
|
||||
try:
|
||||
html = self.lcp.get_html()
|
||||
@@ -358,14 +358,14 @@ as necessary based on the problem config and state.'''
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
|
||||
Returns a json dictionary:
|
||||
{ 'progress_changed' : True/False,
|
||||
'progress' : 'none'/'in_progress'/'done',
|
||||
<other request-specific values here > }
|
||||
'''
|
||||
Returns a json dictionary:
|
||||
{ 'progress_changed' : True/False,
|
||||
'progress' : 'none'/'in_progress'/'done',
|
||||
<other request-specific values here > }
|
||||
'''
|
||||
handlers = {
|
||||
'problem_get': self.get_problem,
|
||||
'problem_check': self.check_problem,
|
||||
@@ -398,7 +398,7 @@ Returns a json dictionary:
|
||||
|
||||
def answer_available(self):
|
||||
''' Is the user allowed to see an answer?
|
||||
'''
|
||||
'''
|
||||
if self.show_answer == '':
|
||||
return False
|
||||
|
||||
@@ -425,14 +425,14 @@ Returns a json dictionary:
|
||||
|
||||
def update_score(self, get):
|
||||
"""
|
||||
Delivers grading response (e.g. from asynchronous code checking) to
|
||||
the capa problem, so its score can be updated
|
||||
Delivers grading response (e.g. from asynchronous code checking) to
|
||||
the capa problem, so its score can be updated
|
||||
|
||||
'get' must have a field 'response' which is a string that contains the
|
||||
grader's response
|
||||
'get' must have a field 'response' which is a string that contains the
|
||||
grader's response
|
||||
|
||||
No ajax return is needed. Return empty dict.
|
||||
"""
|
||||
No ajax return is needed. Return empty dict.
|
||||
"""
|
||||
queuekey = get['queuekey']
|
||||
score_msg = get['xqueue_body']
|
||||
self.lcp.update_score(score_msg, queuekey)
|
||||
@@ -441,10 +441,10 @@ No ajax return is needed. Return empty dict.
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
For the "show answer" button.
|
||||
For the "show answer" button.
|
||||
|
||||
Returns the answers: {'answers' : answers}
|
||||
'''
|
||||
Returns the answers: {'answers' : answers}
|
||||
'''
|
||||
event_info = dict()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
self.system.track_function('show_answer', event_info)
|
||||
@@ -469,18 +469,18 @@ Returns the answers: {'answers' : answers}
|
||||
# Figure out if we should move these to capa_problem?
|
||||
def get_problem(self, get):
|
||||
''' Return results of get_problem_html, as a simple dict for json-ing.
|
||||
{ 'html': <the-html> }
|
||||
{ 'html': <the-html> }
|
||||
|
||||
Used if we want to reconfirm we have the right thing e.g. after
|
||||
several AJAX calls.
|
||||
'''
|
||||
Used if we want to reconfirm we have the right thing e.g. after
|
||||
several AJAX calls.
|
||||
'''
|
||||
return {'html': self.get_problem_html(encapsulate=False)}
|
||||
|
||||
@staticmethod
|
||||
def make_dict_of_responses(get):
|
||||
'''Make dictionary of student responses (aka "answers")
|
||||
get is POST dictionary.
|
||||
'''
|
||||
get is POST dictionary.
|
||||
'''
|
||||
answers = dict()
|
||||
for key in get:
|
||||
# e.g. input_resistor_1 ==> resistor_1
|
||||
@@ -500,11 +500,11 @@ get is POST dictionary.
|
||||
|
||||
def check_problem(self, get):
|
||||
''' Checks whether answers to a problem are correct, and
|
||||
returns a map of correct/incorrect answers:
|
||||
returns a map of correct/incorrect answers:
|
||||
|
||||
{'success' : bool,
|
||||
'contents' : html}
|
||||
'''
|
||||
{'success' : bool,
|
||||
'contents' : html}
|
||||
'''
|
||||
event_info = dict()
|
||||
event_info['state'] = self.lcp.get_state()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
@@ -567,18 +567,18 @@ returns a map of correct/incorrect answers:
|
||||
# 'success' will always be incorrect
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
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)
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
|
||||
return {'success': success,
|
||||
'contents': html,
|
||||
}
|
||||
return {'success': success,
|
||||
'contents': html,
|
||||
}
|
||||
|
||||
def save_problem(self, get):
|
||||
'''
|
||||
@@ -654,9 +654,9 @@ def reset_problem(self, get):
|
||||
|
||||
class CapaDescriptor(RawDescriptor):
|
||||
"""
|
||||
Module implementing problems in the LON-CAPA format,
|
||||
as implemented by capa.capa_problem
|
||||
"""
|
||||
Module implementing problems in the LON-CAPA format,
|
||||
as implemented by capa.capa_problem
|
||||
"""
|
||||
|
||||
module_class = CapaModule
|
||||
|
||||
|
||||
@@ -125,30 +125,6 @@ class @Problem
|
||||
@gentle_alert saveMessage
|
||||
@updateProgress response
|
||||
|
||||
refreshMath: (event, element) =>
|
||||
element = event.target unless element
|
||||
elid = element.id.replace(/^input_/,'')
|
||||
target = "display_" + elid
|
||||
|
||||
# MathJax preprocessor is loaded by 'setupInputTypes'
|
||||
preprocessor_tag = "inputtype_" + elid
|
||||
mathjax_preprocessor = @inputtypeDisplays[preprocessor_tag]
|
||||
|
||||
if jax = MathJax.Hub.getAllJax(target)[0]
|
||||
eqn = $(element).val()
|
||||
if mathjax_preprocessor
|
||||
eqn = mathjax_preprocessor(eqn)
|
||||
MathJax.Hub.Queue(['Text', jax, eqn], [@updateMathML, jax, element])
|
||||
|
||||
return # Explicit return for CoffeeScript
|
||||
|
||||
updateMathML: (jax, element) =>
|
||||
try
|
||||
$("##{element.id}_dynamath").val(jax.root.toMathML '')
|
||||
catch exception
|
||||
throw exception unless exception.restart
|
||||
MathJax.Callback.After [@refreshMath, jax], exception.restart
|
||||
|
||||
refreshAnswers: =>
|
||||
@$('input.schematic').each (index, element) ->
|
||||
element.schematic.update_value()
|
||||
|
||||
@@ -57,9 +57,10 @@ class SelfAssessmentModule(XModule):
|
||||
dom2 = etree.fromstring(definition['data'])
|
||||
|
||||
self.attempts = 0
|
||||
self.max_attempts = None
|
||||
self.max_attempts = 1
|
||||
|
||||
self.max_attempts = self.metadata.get('attempts', None)
|
||||
|
||||
if self.max_attempts is not None:
|
||||
self.max_attempts = int(self.max_attempts)
|
||||
|
||||
@@ -70,7 +71,7 @@ class SelfAssessmentModule(XModule):
|
||||
|
||||
self.name = only_one(dom2.xpath('/problem/@name'))
|
||||
|
||||
self.rubric=etree.tostring(only_one(dom2.xpath("/rubric")))
|
||||
self.rubric=etree.tostring(only_one(dom2.xpath("/rubric/html")))
|
||||
self.problem=etree.tostring(only_one(dom2.xpath("/problem")))
|
||||
|
||||
self.lcp = LoncapaProblem(self.problem, self.location.html_id(),
|
||||
@@ -255,10 +256,7 @@ class SelfAssessmentModule(XModule):
|
||||
handlers = {
|
||||
'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,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -274,7 +272,7 @@ class SelfAssessmentModule(XModule):
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def closed(self):
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
if self.attempts == self.max_attempts:
|
||||
return True
|
||||
if self.close_date is not None and datetime.datetime.utcnow() > self.close_date:
|
||||
@@ -319,9 +317,10 @@ class SelfAssessmentModule(XModule):
|
||||
'''
|
||||
return {'html': self.get_problem_html(encapsulate=False)}
|
||||
|
||||
@staticmethod
|
||||
@staticmethod
|
||||
def make_dict_of_responses(get):
|
||||
'''Make dictionary of student responses (aka "answers")
|
||||
'''
|
||||
Make dictionary of student responses (aka "answers")
|
||||
get is POST dictionary.
|
||||
'''
|
||||
answers = dict()
|
||||
@@ -355,9 +354,9 @@ class SelfAssessmentModule(XModule):
|
||||
answers = self.make_dict_of_responses(get)
|
||||
event_info['answers'] = convert_files_to_filenames(answers)
|
||||
|
||||
parsed_answer=False
|
||||
if(answer[answers.keys()[0]]=="True"):
|
||||
parsed_answer=True
|
||||
parsed_answer="incorrect"
|
||||
if(answer[answers.keys()[1]]=="Correct"):
|
||||
parsed_answer="correct"
|
||||
|
||||
# Too late. Cannot submit
|
||||
if self.closed():
|
||||
@@ -445,137 +444,36 @@ class SelfAssessmentModule(XModule):
|
||||
return {'success': True}
|
||||
|
||||
|
||||
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
class SelfAssessmentDescriptor(RawDescriptor):
|
||||
"""
|
||||
Module for putting raw html in a course
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = HtmlModule
|
||||
filename_extension = "xml"
|
||||
template_dir_name = "html"
|
||||
stores_state=True
|
||||
has_score=True
|
||||
module_class = SelfAssessmentModule
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/edit.coffee')]}
|
||||
js_module_name = "HTMLEditingDescriptor"
|
||||
stores_state = True
|
||||
has_score = True
|
||||
template_dir_name = 'selfassessment'
|
||||
|
||||
# VS[compat] TODO (cpennington): Delete this method once all fall 2012 course
|
||||
# are being edited in the cms
|
||||
# Capa modules have some additional metadata:
|
||||
# TODO (vshnayder): do problems have any other metadata? Do they
|
||||
# actually use type and points?
|
||||
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
@classmethod
|
||||
def backcompat_paths(cls, path):
|
||||
if path.endswith('.html.xml'):
|
||||
path = path[:-9] + '.html' # backcompat--look for html instead of xml
|
||||
if path.endswith('.html.html'):
|
||||
path = path[:-5] # some people like to include .html in filenames..
|
||||
candidates = []
|
||||
while os.sep in path:
|
||||
candidates.append(path)
|
||||
_, _, path = path.partition(os.sep)
|
||||
return [
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
|
||||
# also look for .html versions instead of .xml
|
||||
nc = []
|
||||
for candidate in candidates:
|
||||
if candidate.endswith('.xml'):
|
||||
nc.append(candidate[:-4] + '.html')
|
||||
return candidates + nc
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CapaDescriptor, self).__init__(*args, **kwargs)
|
||||
|
||||
# NOTE: html descriptors are special. We do not want to parse and
|
||||
# export them ourselves, because that can break things (e.g. lxml
|
||||
# adds body tags when it exports, but they should just be html
|
||||
# snippets that will be included in the middle of pages.
|
||||
|
||||
@classmethod
|
||||
def load_definition(cls, xml_object, system, location):
|
||||
'''Load a descriptor from the specified xml_object:
|
||||
|
||||
If there is a filename attribute, load it as a string, and
|
||||
log a warning if it is not parseable by etree.HTMLParser.
|
||||
|
||||
If there is not a filename attribute, the definition is the body
|
||||
of the xml_object, without the root tag (do not want <html> in the
|
||||
middle of a page)
|
||||
'''
|
||||
filename = xml_object.get('filename')
|
||||
if filename is None:
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return {'data': stringify_children(definition_xml)}
|
||||
weight_string = self.metadata.get('weight', None)
|
||||
if weight_string:
|
||||
self.weight = float(weight_string)
|
||||
else:
|
||||
# html is special. cls.filename_extension is 'xml', but
|
||||
# if 'filename' is in the definition, that means to load
|
||||
# from .html
|
||||
# 'filename' in html pointers is a relative path
|
||||
# (not same as 'html/blah.html' when the pointer is in a directory itself)
|
||||
pointer_path = "{category}/{url_path}".format(category='html',
|
||||
url_path=name_to_pathname(location.name))
|
||||
base = path(pointer_path).dirname()
|
||||
#log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
|
||||
filepath = "{base}/{name}.html".format(base=base, name=filename)
|
||||
#log.debug("looking for html file for {0} at {1}".format(location, filepath))
|
||||
|
||||
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
# give the class a chance to fix it up. The file will be written out
|
||||
# again in the correct format. This should go away once the CMS is
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
#log.debug("candidates = {0}".format(candidates))
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
html = file.read()
|
||||
# Log a warning if we can't parse the file, but don't error
|
||||
if not check_html(html):
|
||||
msg = "Couldn't parse html in {0}.".format(filepath)
|
||||
log.warning(msg)
|
||||
system.error_tracker("Warning: " + msg)
|
||||
|
||||
definition = {'data': html}
|
||||
|
||||
# TODO (ichuang): remove this after migration
|
||||
# for Fall 2012 LMS migration: keep filename (and unmangled filename)
|
||||
definition['filename'] = [ filepath, filename ]
|
||||
|
||||
return definition
|
||||
|
||||
except (ResourceNotFoundError) as err:
|
||||
msg = 'Unable to load file contents at path {0}: {1} '.format(
|
||||
filepath, err)
|
||||
# add more info and re-raise
|
||||
raise Exception(msg), None, sys.exc_info()[2]
|
||||
|
||||
# TODO (vshnayder): make export put things in the right places.
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''If the contents are valid xml, write them to filename.xml. Otherwise,
|
||||
write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
|
||||
string to filename.html.
|
||||
'''
|
||||
try:
|
||||
return etree.fromstring(self.definition['data'])
|
||||
except etree.XMLSyntaxError:
|
||||
pass
|
||||
|
||||
# Not proper format. Write html to file, return an empty tag
|
||||
pathname = name_to_pathname(self.url_name)
|
||||
pathdir = path(pathname).dirname()
|
||||
filepath = u'{category}/{pathname}.html'.format(category=self.category,
|
||||
pathname=pathname)
|
||||
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(self.definition['data'])
|
||||
|
||||
# write out the relative name
|
||||
relname = path(pathname).basename()
|
||||
|
||||
elt = etree.Element('html')
|
||||
elt.set("filename", relname)
|
||||
return elt
|
||||
self.weight = None
|
||||
Reference in New Issue
Block a user