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+?)
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)
Returns a datetime.timedelta parsed from the string
"""
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
"""
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.
Returns a json dictionary:
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > }
'''
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 > }
'''
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
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.
module_class = SelfAssessmentModule
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]
# TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms
@classmethod
def load_definition(cls, xml_object, system, location):
'''Load a descriptor from the specified xml_object:
def backcompat_paths(cls, path):
return [
'problems/' + path[8:],
path[8:],
]
If there is a filename attribute, load it as a string, and
log a warning if it is not parseable by etree.HTMLParser.
def __init__(self, *args, **kwargs):
super(CapaDescriptor, self).__init__(*args, **kwargs)
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
\ No newline at end of file
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