Commit 32a8ed7b by Vik Paruchuri

adding in self assessment grader

parent b74a484f
...@@ -29,10 +29,10 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) ...@@ -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): def only_one(lst, default="", process=lambda x: x):
""" """
If lst is empty, returns default If lst is empty, returns default
If lst has a single element, applies process to that element and returns it If lst has a single element, applies process to that element and returns it
Otherwise, raises an exeception Otherwise, raises an exeception
""" """
if len(lst) == 0: if len(lst) == 0:
return default return default
elif len(lst) == 1: elif len(lst) == 1:
...@@ -43,14 +43,14 @@ Otherwise, raises an exeception ...@@ -43,14 +43,14 @@ Otherwise, raises an exeception
def parse_timedelta(time_str): def parse_timedelta(time_str):
""" """
time_str: A string with the following components: time_str: A string with the following components:
<D> day[s] (optional) <D> day[s] (optional)
<H> hour[s] (optional) <H> hour[s] (optional)
<M> minute[s] (optional) <M> minute[s] (optional)
<S> second[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) parts = TIMEDELTA_REGEX.match(time_str)
if not parts: if not parts:
return return
...@@ -71,9 +71,9 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -71,9 +71,9 @@ class ComplexEncoder(json.JSONEncoder):
class CapaModule(XModule): class CapaModule(XModule):
''' '''
An XModule implementing LonCapa format problems, implemented by way of An XModule implementing LonCapa format problems, implemented by way of
capa.capa_problem.LoncapaProblem capa.capa_problem.LoncapaProblem
''' '''
icon_class = 'problem' icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
...@@ -175,9 +175,9 @@ capa.capa_problem.LoncapaProblem ...@@ -175,9 +175,9 @@ capa.capa_problem.LoncapaProblem
@property @property
def rerandomize(self): def rerandomize(self):
""" """
Property accessor that returns self.metadata['rerandomize'] in a Property accessor that returns self.metadata['rerandomize'] in a
canonical form canonical form
""" """
rerandomize = self.metadata.get('rerandomize', 'always') rerandomize = self.metadata.get('rerandomize', 'always')
if rerandomize in ("", "always", "true"): if rerandomize in ("", "always", "true"):
return "always" return "always"
...@@ -203,7 +203,7 @@ canonical form ...@@ -203,7 +203,7 @@ canonical form
def get_progress(self): def get_progress(self):
''' For now, just return score / max_score ''' For now, just return score / max_score
''' '''
d = self.get_score() d = self.get_score()
score = d['score'] score = d['score']
total = d['total'] total = d['total']
...@@ -224,7 +224,7 @@ canonical form ...@@ -224,7 +224,7 @@ canonical form
def get_problem_html(self, encapsulate=True): def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons '''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: try:
html = self.lcp.get_html() html = self.lcp.get_html()
...@@ -358,14 +358,14 @@ as necessary based on the problem config and state.''' ...@@ -358,14 +358,14 @@ as necessary based on the problem config and state.'''
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' '''
This is called by courseware.module_render, to handle an AJAX call. This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST. "get" is request.POST.
Returns a json dictionary: Returns a json dictionary:
{ 'progress_changed' : True/False, { 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done', 'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > } <other request-specific values here > }
''' '''
handlers = { handlers = {
'problem_get': self.get_problem, 'problem_get': self.get_problem,
'problem_check': self.check_problem, 'problem_check': self.check_problem,
...@@ -398,7 +398,7 @@ Returns a json dictionary: ...@@ -398,7 +398,7 @@ Returns a json dictionary:
def answer_available(self): def answer_available(self):
''' Is the user allowed to see an answer? ''' Is the user allowed to see an answer?
''' '''
if self.show_answer == '': if self.show_answer == '':
return False return False
...@@ -425,14 +425,14 @@ Returns a json dictionary: ...@@ -425,14 +425,14 @@ Returns a json dictionary:
def update_score(self, get): def update_score(self, get):
""" """
Delivers grading response (e.g. from asynchronous code checking) to Delivers grading response (e.g. from asynchronous code checking) to
the capa problem, so its score can be updated the capa problem, so its score can be updated
'get' must have a field 'response' which is a string that contains the 'get' must have a field 'response' which is a string that contains the
grader's response grader's response
No ajax return is needed. Return empty dict. No ajax return is needed. Return empty dict.
""" """
queuekey = get['queuekey'] queuekey = get['queuekey']
score_msg = get['xqueue_body'] score_msg = get['xqueue_body']
self.lcp.update_score(score_msg, queuekey) self.lcp.update_score(score_msg, queuekey)
...@@ -441,10 +441,10 @@ No ajax return is needed. Return empty dict. ...@@ -441,10 +441,10 @@ No ajax return is needed. Return empty dict.
def get_answer(self, get): 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 = dict()
event_info['problem_id'] = self.location.url() event_info['problem_id'] = self.location.url()
self.system.track_function('show_answer', event_info) self.system.track_function('show_answer', event_info)
...@@ -469,18 +469,18 @@ Returns the answers: {'answers' : answers} ...@@ -469,18 +469,18 @@ Returns the answers: {'answers' : answers}
# Figure out if we should move these to capa_problem? # Figure out if we should move these to capa_problem?
def get_problem(self, get): def get_problem(self, get):
''' Return results of get_problem_html, as a simple dict for json-ing. ''' 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 Used if we want to reconfirm we have the right thing e.g. after
several AJAX calls. several AJAX calls.
''' '''
return {'html': self.get_problem_html(encapsulate=False)} return {'html': self.get_problem_html(encapsulate=False)}
@staticmethod @staticmethod
def make_dict_of_responses(get): def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers") '''Make dictionary of student responses (aka "answers")
get is POST dictionary. get is POST dictionary.
''' '''
answers = dict() answers = dict()
for key in get: for key in get:
# e.g. input_resistor_1 ==> resistor_1 # e.g. input_resistor_1 ==> resistor_1
...@@ -500,11 +500,11 @@ get is POST dictionary. ...@@ -500,11 +500,11 @@ get is POST dictionary.
def check_problem(self, get): def check_problem(self, get):
''' Checks whether answers to a problem are correct, and ''' 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, {'success' : bool,
'contents' : html} 'contents' : html}
''' '''
event_info = dict() event_info = dict()
event_info['state'] = self.lcp.get_state() event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url() event_info['problem_id'] = self.location.url()
...@@ -567,16 +567,16 @@ returns a map of correct/incorrect answers: ...@@ -567,16 +567,16 @@ returns a map of correct/incorrect answers:
# 'success' will always be incorrect # 'success' will always be incorrect
event_info['correct_map'] = correct_map.get_dict() event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success event_info['success'] = success
event_info['attempts'] = self.attempts event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info) self.system.track_function('save_problem_check', event_info)
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state()) self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML # render problem into HTML
html = self.get_problem_html(encapsulate=False) html = self.get_problem_html(encapsulate=False)
return {'success': success, return {'success': success,
'contents': html, 'contents': html,
} }
...@@ -654,9 +654,9 @@ def reset_problem(self, get): ...@@ -654,9 +654,9 @@ def reset_problem(self, get):
class CapaDescriptor(RawDescriptor): class CapaDescriptor(RawDescriptor):
""" """
Module implementing problems in the LON-CAPA format, Module implementing problems in the LON-CAPA format,
as implemented by capa.capa_problem as implemented by capa.capa_problem
""" """
module_class = CapaModule module_class = CapaModule
......
...@@ -125,30 +125,6 @@ class @Problem ...@@ -125,30 +125,6 @@ class @Problem
@gentle_alert saveMessage @gentle_alert saveMessage
@updateProgress response @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: => refreshAnswers: =>
@$('input.schematic').each (index, element) -> @$('input.schematic').each (index, element) ->
element.schematic.update_value() element.schematic.update_value()
......
...@@ -57,9 +57,10 @@ class SelfAssessmentModule(XModule): ...@@ -57,9 +57,10 @@ class SelfAssessmentModule(XModule):
dom2 = etree.fromstring(definition['data']) dom2 = etree.fromstring(definition['data'])
self.attempts = 0 self.attempts = 0
self.max_attempts = None self.max_attempts = 1
self.max_attempts = self.metadata.get('attempts', None) self.max_attempts = self.metadata.get('attempts', None)
if self.max_attempts is not None: if self.max_attempts is not None:
self.max_attempts = int(self.max_attempts) self.max_attempts = int(self.max_attempts)
...@@ -70,7 +71,7 @@ class SelfAssessmentModule(XModule): ...@@ -70,7 +71,7 @@ class SelfAssessmentModule(XModule):
self.name = only_one(dom2.xpath('/problem/@name')) 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.problem=etree.tostring(only_one(dom2.xpath("/problem")))
self.lcp = LoncapaProblem(self.problem, self.location.html_id(), self.lcp = LoncapaProblem(self.problem, self.location.html_id(),
...@@ -255,10 +256,7 @@ class SelfAssessmentModule(XModule): ...@@ -255,10 +256,7 @@ class SelfAssessmentModule(XModule):
handlers = { handlers = {
'problem_get': self.get_problem, 'problem_get': self.get_problem,
'problem_check': self.check_problem, 'problem_check': self.check_problem,
'problem_reset': self.reset_problem,
'problem_save': self.save_problem, 'problem_save': self.save_problem,
'problem_show': self.get_answer,
'score_update': self.update_score,
} }
if dispatch not in handlers: if dispatch not in handlers:
...@@ -321,7 +319,8 @@ class SelfAssessmentModule(XModule): ...@@ -321,7 +319,8 @@ class SelfAssessmentModule(XModule):
@staticmethod @staticmethod
def make_dict_of_responses(get): def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers") '''
Make dictionary of student responses (aka "answers")
get is POST dictionary. get is POST dictionary.
''' '''
answers = dict() answers = dict()
...@@ -355,9 +354,9 @@ class SelfAssessmentModule(XModule): ...@@ -355,9 +354,9 @@ class SelfAssessmentModule(XModule):
answers = self.make_dict_of_responses(get) answers = self.make_dict_of_responses(get)
event_info['answers'] = convert_files_to_filenames(answers) event_info['answers'] = convert_files_to_filenames(answers)
parsed_answer=False parsed_answer="incorrect"
if(answer[answers.keys()[0]]=="True"): if(answer[answers.keys()[1]]=="Correct"):
parsed_answer=True parsed_answer="correct"
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed():
...@@ -445,137 +444,36 @@ class SelfAssessmentModule(XModule): ...@@ -445,137 +444,36 @@ class SelfAssessmentModule(XModule):
return {'success': True} return {'success': True}
class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): class SelfAssessmentDescriptor(RawDescriptor):
""" """
Module for putting raw html in a course Module for putting raw html in a course
""" """
mako_template = "widgets/html-edit.html" module_class = SelfAssessmentModule
module_class = HtmlModule
filename_extension = "xml"
template_dir_name = "html"
stores_state=True
has_score=True
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
# 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)
# 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
# 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)}
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))
stores_state = True
has_score = True
template_dir_name = 'selfassessment'
# 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] # VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path, # TODO (cpennington): Delete this method once all fall 2012 course are being
# give the class a chance to fix it up. The file will be written out # edited in the cms
# again in the correct format. This should go away once the CMS is @classmethod
# online and has imported all current (fall 2012) courses from xml def backcompat_paths(cls, path):
if not system.resources_fs.exists(filepath): return [
candidates = cls.backcompat_paths(filepath) 'problems/' + path[8:],
#log.debug("candidates = {0}".format(candidates)) path[8:],
for candidate in candidates: ]
if system.resources_fs.exists(candidate):
filepath = candidate
break
try: def __init__(self, *args, **kwargs):
with system.resources_fs.open(filepath) as file: super(CapaDescriptor, self).__init__(*args, **kwargs)
html = file.read()
# Log a warning if we can't parse the file, but don't error weight_string = self.metadata.get('weight', None)
if not check_html(html): if weight_string:
msg = "Couldn't parse html in {0}.".format(filepath) self.weight = float(weight_string)
log.warning(msg) else:
system.error_tracker("Warning: " + msg) self.weight = None
\ No newline at end of file
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment