Commit 58e8f7db by Diana Huang

- Pep8 and pylint fixes

- beginnings of new Matlab input type
- update progress after getting a response from xqueue
parent 05ba082c
...@@ -44,7 +44,6 @@ from lxml import etree ...@@ -44,7 +44,6 @@ from lxml import etree
import re import re
import shlex # for splitting quoted strings import shlex # for splitting quoted strings
import sys import sys
import os
import pyparsing import pyparsing
from .registry import TagRegistry from .registry import TagRegistry
...@@ -97,7 +96,8 @@ class Attribute(object): ...@@ -97,7 +96,8 @@ class Attribute(object):
""" """
val = element.get(self.name) val = element.get(self.name)
if self.default == self._sentinel and val is None: if self.default == self._sentinel and val is None:
raise ValueError('Missing required attribute {0}.'.format(self.name)) raise ValueError(
'Missing required attribute {0}.'.format(self.name))
if val is None: if val is None:
# not required, so return default # not required, so return default
...@@ -149,7 +149,8 @@ class InputTypeBase(object): ...@@ -149,7 +149,8 @@ class InputTypeBase(object):
self.id = state.get('id', xml.get('id')) self.id = state.get('id', xml.get('id'))
if self.id is None: if self.id is None:
raise ValueError("input id state is None. xml is {0}".format(etree.tostring(xml))) raise ValueError("input id state is None. xml is {0}".format(
etree.tostring(xml)))
self.value = state.get('value', '') self.value = state.get('value', '')
...@@ -169,14 +170,15 @@ class InputTypeBase(object): ...@@ -169,14 +170,15 @@ class InputTypeBase(object):
self.process_requirements() self.process_requirements()
# Call subclass "constructor" -- means they don't have to worry about calling # Call subclass "constructor" -- means they don't have to worry about calling
# super().__init__, and are isolated from changes to the input constructor interface. # super().__init__, and are isolated from changes to the input
# constructor interface.
self.setup() self.setup()
except Exception as err: except Exception as err:
# Something went wrong: add xml to message, but keep the traceback # Something went wrong: add xml to message, but keep the traceback
msg = "Error in xml '{x}': {err} ".format(x=etree.tostring(xml), err=str(err)) msg = "Error in xml '{x}': {err} ".format(
x=etree.tostring(xml), err=str(err))
raise Exception, msg, sys.exc_info()[2] raise Exception, msg, sys.exc_info()[2]
@classmethod @classmethod
def get_attributes(cls): def get_attributes(cls):
""" """
...@@ -186,7 +188,6 @@ class InputTypeBase(object): ...@@ -186,7 +188,6 @@ class InputTypeBase(object):
""" """
return [] return []
def process_requirements(self): def process_requirements(self):
""" """
Subclasses can declare lists of required and optional attributes. This Subclasses can declare lists of required and optional attributes. This
...@@ -196,7 +197,8 @@ class InputTypeBase(object): ...@@ -196,7 +197,8 @@ class InputTypeBase(object):
Processes attributes, putting the results in the self.loaded_attributes dictionary. Also creates a set Processes attributes, putting the results in the self.loaded_attributes dictionary. Also creates a set
self.to_render, containing the names of attributes that should be included in the context by default. self.to_render, containing the names of attributes that should be included in the context by default.
""" """
# Use local dicts and sets so that if there are exceptions, we don't end up in a partially-initialized state. # Use local dicts and sets so that if there are exceptions, we don't
# end up in a partially-initialized state.
loaded = {} loaded = {}
to_render = set() to_render = set()
for a in self.get_attributes(): for a in self.get_attributes():
...@@ -226,7 +228,7 @@ class InputTypeBase(object): ...@@ -226,7 +228,7 @@ class InputTypeBase(object):
get: a dictionary containing the data that was sent with the ajax call get: a dictionary containing the data that was sent with the ajax call
Output: Output:
a dictionary object that can be serialized into JSON. This will be sent back to the Javascript. a dictionary object that can be serialized into JSON. This will be sent back to the Javascript.
""" """
pass pass
...@@ -247,8 +249,9 @@ class InputTypeBase(object): ...@@ -247,8 +249,9 @@ class InputTypeBase(object):
'value': self.value, 'value': self.value,
'status': self.status, 'status': self.status,
'msg': self.msg, 'msg': self.msg,
} }
context.update((a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render) context.update((a, v) for (
a, v) in self.loaded_attributes.iteritems() if a in self.to_render)
context.update(self._extra_context()) context.update(self._extra_context())
return context return context
...@@ -371,7 +374,6 @@ class ChoiceGroup(InputTypeBase): ...@@ -371,7 +374,6 @@ class ChoiceGroup(InputTypeBase):
return [Attribute("show_correctness", "always"), return [Attribute("show_correctness", "always"),
Attribute("submitted_message", "Answer received.")] Attribute("submitted_message", "Answer received.")]
def _extra_context(self): def _extra_context(self):
return {'input_type': self.html_input_type, return {'input_type': self.html_input_type,
'choices': self.choices, 'choices': self.choices,
...@@ -436,7 +438,6 @@ class JavascriptInput(InputTypeBase): ...@@ -436,7 +438,6 @@ class JavascriptInput(InputTypeBase):
Attribute('display_class', None), Attribute('display_class', None),
Attribute('display_file', None), ] Attribute('display_file', None), ]
def setup(self): def setup(self):
# Need to provide a value that JSON can parse if there is no # Need to provide a value that JSON can parse if there is no
# student-supplied value yet. # student-supplied value yet.
...@@ -459,7 +460,6 @@ class TextLine(InputTypeBase): ...@@ -459,7 +460,6 @@ class TextLine(InputTypeBase):
template = "textline.html" template = "textline.html"
tags = ['textline'] tags = ['textline']
@classmethod @classmethod
def get_attributes(cls): def get_attributes(cls):
""" """
...@@ -474,12 +474,12 @@ class TextLine(InputTypeBase): ...@@ -474,12 +474,12 @@ class TextLine(InputTypeBase):
# Attributes below used in setup(), not rendered directly. # Attributes below used in setup(), not rendered directly.
Attribute('math', None, render=False), Attribute('math', None, render=False),
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x # TODO: 'dojs' flag is temporary, for backwards compatibility with
# 8.02x
Attribute('dojs', None, render=False), Attribute('dojs', None, render=False),
Attribute('preprocessorClassName', None, render=False), Attribute('preprocessorClassName', None, render=False),
Attribute('preprocessorSrc', None, render=False), Attribute('preprocessorSrc', None, render=False),
] ]
def setup(self): def setup(self):
self.do_math = bool(self.loaded_attributes['math'] or self.do_math = bool(self.loaded_attributes['math'] or
...@@ -490,12 +490,12 @@ class TextLine(InputTypeBase): ...@@ -490,12 +490,12 @@ class TextLine(InputTypeBase):
self.preprocessor = None self.preprocessor = None
if self.do_math: if self.do_math:
# Preprocessor to insert between raw input and Mathjax # Preprocessor to insert between raw input and Mathjax
self.preprocessor = {'class_name': self.loaded_attributes['preprocessorClassName'], self.preprocessor = {
'script_src': self.loaded_attributes['preprocessorSrc']} 'class_name': self.loaded_attributes['preprocessorClassName'],
'script_src': self.loaded_attributes['preprocessorSrc']}
if None in self.preprocessor.values(): if None in self.preprocessor.values():
self.preprocessor = None self.preprocessor = None
def _extra_context(self): def _extra_context(self):
return {'do_math': self.do_math, return {'do_math': self.do_math,
'preprocessor': self.preprocessor, } 'preprocessor': self.preprocessor, }
...@@ -539,7 +539,8 @@ class FileSubmission(InputTypeBase): ...@@ -539,7 +539,8 @@ class FileSubmission(InputTypeBase):
""" """
# Check if problem has been queued # Check if problem has been queued
self.queue_len = 0 self.queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of queue # Flag indicating that the problem has been queued, 'msg' is length of
# queue
if self.status == 'incomplete': if self.status == 'incomplete':
self.status = 'queued' self.status = 'queued'
self.queue_len = self.msg self.queue_len = self.msg
...@@ -547,7 +548,6 @@ class FileSubmission(InputTypeBase): ...@@ -547,7 +548,6 @@ class FileSubmission(InputTypeBase):
def _extra_context(self): def _extra_context(self):
return {'queue_len': self.queue_len, } return {'queue_len': self.queue_len, }
return context
registry.register(FileSubmission) registry.register(FileSubmission)
...@@ -562,8 +562,9 @@ class CodeInput(InputTypeBase): ...@@ -562,8 +562,9 @@ class CodeInput(InputTypeBase):
template = "codeinput.html" template = "codeinput.html"
tags = ['codeinput', tags = ['codeinput',
'textbox', # Another (older) name--at some point we may want to make it use a 'textbox',
# non-codemirror editor. # Another (older) name--at some point we may want to make it use a
# non-codemirror editor.
] ]
# pulled out for testing # pulled out for testing
...@@ -590,13 +591,15 @@ class CodeInput(InputTypeBase): ...@@ -590,13 +591,15 @@ class CodeInput(InputTypeBase):
""" """
Implement special logic: handle queueing state, and default input. Implement special logic: handle queueing state, and default input.
""" """
# if no student input yet, then use the default input given by the problem # if no student input yet, then use the default input given by the
# problem
if not self.value: if not self.value:
self.value = self.xml.text self.value = self.xml.text
# Check if problem has been queued # Check if problem has been queued
self.queue_len = 0 self.queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of queue # Flag indicating that the problem has been queued, 'msg' is length of
# queue
if self.status == 'incomplete': if self.status == 'incomplete':
self.status = 'queued' self.status = 'queued'
self.queue_len = self.msg self.queue_len = self.msg
...@@ -610,8 +613,67 @@ registry.register(CodeInput) ...@@ -610,8 +613,67 @@ registry.register(CodeInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class MatlabInput(CodeInput):
'''
InputType for handling Matlab code input
'''
template = "matlabinput.html"
tags = ['matlabinput']
# pulled out for testing
submitted_msg = ("Submitted. As soon as your submission is"
" graded, this message will be replaced with the grader's feedback.")
def setup(self):
'''
Handle matlab-specific parsing
'''
xml = self.xml
self.plot_payload = xml.findtext('./plot_payload')
# if no student input yet, then use the default input given by the
# problem
if not self.value:
self.value = self.xml.text
# Check if problem has been queued
self.queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of
# queue
if self.status == 'incomplete':
self.status = 'queued'
self.queue_len = self.msg
self.msg = self.submitted_msg
def handle_ajax(self, dispatch, get):
if dispatch == 'plot':
# put the data in the queue and ship it off
pass
elif dispatch == 'display':
# render the response
pass
def plot_data(self, get):
''' send data via xqueue to the mathworks backend'''
# only send data if xqueue exists
if self.system.xqueue is not None:
pass
registry.register(MatlabInput)
#-----------------------------------------------------------------------------
class Schematic(InputTypeBase): class Schematic(InputTypeBase):
""" """
InputType for the schematic editor
""" """
template = "schematicinput.html" template = "schematicinput.html"
...@@ -630,7 +692,6 @@ class Schematic(InputTypeBase): ...@@ -630,7 +692,6 @@ class Schematic(InputTypeBase):
Attribute('initial_value', None), Attribute('initial_value', None),
Attribute('submit_analyses', None), ] Attribute('submit_analyses', None), ]
return context
registry.register(Schematic) registry.register(Schematic)
...@@ -660,12 +721,12 @@ class ImageInput(InputTypeBase): ...@@ -660,12 +721,12 @@ class ImageInput(InputTypeBase):
Attribute('height'), Attribute('height'),
Attribute('width'), ] Attribute('width'), ]
def setup(self): def setup(self):
""" """
if value is of the form [x,y] then parse it and send along coordinates of previous answer if value is of the form [x,y] then parse it and send along coordinates of previous answer
""" """
m = re.match('\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) m = re.match('\[([0-9]+),([0-9]+)]',
self.value.strip().replace(' ', ''))
if m: if m:
# Note: we subtract 15 to compensate for the size of the dot on the screen. # Note: we subtract 15 to compensate for the size of the dot on the screen.
# (is a 30x30 image--lms/static/green-pointer.png). # (is a 30x30 image--lms/static/green-pointer.png).
...@@ -673,7 +734,6 @@ class ImageInput(InputTypeBase): ...@@ -673,7 +734,6 @@ class ImageInput(InputTypeBase):
else: else:
(self.gx, self.gy) = (0, 0) (self.gx, self.gy) = (0, 0)
def _extra_context(self): def _extra_context(self):
return {'gx': self.gx, return {'gx': self.gx,
...@@ -730,7 +790,7 @@ class VseprInput(InputTypeBase): ...@@ -730,7 +790,7 @@ class VseprInput(InputTypeBase):
registry.register(VseprInput) registry.register(VseprInput)
#-------------------------------------------------------------------------------- #-------------------------------------------------------------------------
class ChemicalEquationInput(InputTypeBase): class ChemicalEquationInput(InputTypeBase):
...@@ -794,7 +854,8 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -794,7 +854,8 @@ class ChemicalEquationInput(InputTypeBase):
result['error'] = "Couldn't parse formula: {0}".format(p) result['error'] = "Couldn't parse formula: {0}".format(p)
except Exception: except Exception:
# this is unexpected, so log # this is unexpected, so log
log.warning("Error while previewing chemical formula", exc_info=True) log.warning(
"Error while previewing chemical formula", exc_info=True)
result['error'] = "Error while rendering preview" result['error'] = "Error while rendering preview"
return result return result
...@@ -843,16 +904,16 @@ class DragAndDropInput(InputTypeBase): ...@@ -843,16 +904,16 @@ class DragAndDropInput(InputTypeBase):
'can_reuse': ""} 'can_reuse': ""}
tag_attrs['target'] = {'id': Attribute._sentinel, tag_attrs['target'] = {'id': Attribute._sentinel,
'x': Attribute._sentinel, 'x': Attribute._sentinel,
'y': Attribute._sentinel, 'y': Attribute._sentinel,
'w': Attribute._sentinel, 'w': Attribute._sentinel,
'h': Attribute._sentinel} 'h': Attribute._sentinel}
dic = dict() dic = dict()
for attr_name in tag_attrs[tag_type].keys(): for attr_name in tag_attrs[tag_type].keys():
dic[attr_name] = Attribute(attr_name, dic[attr_name] = Attribute(attr_name,
default=tag_attrs[tag_type][attr_name]).parse_from_xml(tag) default=tag_attrs[tag_type][attr_name]).parse_from_xml(tag)
if tag_type == 'draggable' and not self.no_labels: if tag_type == 'draggable' and not self.no_labels:
dic['label'] = dic['label'] or dic['id'] dic['label'] = dic['label'] or dic['id']
...@@ -865,7 +926,7 @@ class DragAndDropInput(InputTypeBase): ...@@ -865,7 +926,7 @@ class DragAndDropInput(InputTypeBase):
# add labels to images?: # add labels to images?:
self.no_labels = Attribute('no_labels', self.no_labels = Attribute('no_labels',
default="False").parse_from_xml(self.xml) default="False").parse_from_xml(self.xml)
to_js = dict() to_js = dict()
...@@ -874,16 +935,16 @@ class DragAndDropInput(InputTypeBase): ...@@ -874,16 +935,16 @@ class DragAndDropInput(InputTypeBase):
# outline places on image where to drag adn drop # outline places on image where to drag adn drop
to_js['target_outline'] = Attribute('target_outline', to_js['target_outline'] = Attribute('target_outline',
default="False").parse_from_xml(self.xml) default="False").parse_from_xml(self.xml)
# one draggable per target? # one draggable per target?
to_js['one_per_target'] = Attribute('one_per_target', to_js['one_per_target'] = Attribute('one_per_target',
default="True").parse_from_xml(self.xml) default="True").parse_from_xml(self.xml)
# list of draggables # list of draggables
to_js['draggables'] = [parse(draggable, 'draggable') for draggable in to_js['draggables'] = [parse(draggable, 'draggable') for draggable in
self.xml.iterchildren('draggable')] self.xml.iterchildren('draggable')]
# list of targets # list of targets
to_js['targets'] = [parse(target, 'target') for target in to_js['targets'] = [parse(target, 'target') for target in
self.xml.iterchildren('target')] self.xml.iterchildren('target')]
# custom background color for labels: # custom background color for labels:
label_bg_color = Attribute('label_bg_color', label_bg_color = Attribute('label_bg_color',
...@@ -896,7 +957,7 @@ class DragAndDropInput(InputTypeBase): ...@@ -896,7 +957,7 @@ class DragAndDropInput(InputTypeBase):
registry.register(DragAndDropInput) registry.register(DragAndDropInput)
#-------------------------------------------------------------------------------------------------------------------- #-------------------------------------------------------------------------
class EditAMoleculeInput(InputTypeBase): class EditAMoleculeInput(InputTypeBase):
...@@ -934,6 +995,7 @@ registry.register(EditAMoleculeInput) ...@@ -934,6 +995,7 @@ registry.register(EditAMoleculeInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class DesignProtein2dInput(InputTypeBase): class DesignProtein2dInput(InputTypeBase):
""" """
An input type for design of a protein in 2D. Integrates with the Protex java applet. An input type for design of a protein in 2D. Integrates with the Protex java applet.
...@@ -969,6 +1031,7 @@ registry.register(DesignProtein2dInput) ...@@ -969,6 +1031,7 @@ registry.register(DesignProtein2dInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class EditAGeneInput(InputTypeBase): class EditAGeneInput(InputTypeBase):
""" """
An input type for editing a gene. Integrates with the genex java applet. An input type for editing a gene. Integrates with the genex java applet.
...@@ -1005,6 +1068,7 @@ registry.register(EditAGeneInput) ...@@ -1005,6 +1068,7 @@ registry.register(EditAGeneInput)
#--------------------------------------------------------------------- #---------------------------------------------------------------------
class AnnotationInput(InputTypeBase): class AnnotationInput(InputTypeBase):
""" """
Input type for annotations: students can enter some notes or other text Input type for annotations: students can enter some notes or other text
...@@ -1037,13 +1101,14 @@ class AnnotationInput(InputTypeBase): ...@@ -1037,13 +1101,14 @@ class AnnotationInput(InputTypeBase):
def setup(self): def setup(self):
xml = self.xml xml = self.xml
self.debug = False # set to True to display extra debug info with input self.debug = False # set to True to display extra debug info with input
self.return_to_annotation = True # return only works in conjunction with annotatable xmodule self.return_to_annotation = True # return only works in conjunction with annotatable xmodule
self.title = xml.findtext('./title', 'Annotation Exercise') self.title = xml.findtext('./title', 'Annotation Exercise')
self.text = xml.findtext('./text') self.text = xml.findtext('./text')
self.comment = xml.findtext('./comment') self.comment = xml.findtext('./comment')
self.comment_prompt = xml.findtext('./comment_prompt', 'Type a commentary below:') self.comment_prompt = xml.findtext(
'./comment_prompt', 'Type a commentary below:')
self.tag_prompt = xml.findtext('./tag_prompt', 'Select one tag:') self.tag_prompt = xml.findtext('./tag_prompt', 'Select one tag:')
self.options = self._find_options() self.options = self._find_options()
...@@ -1061,7 +1126,7 @@ class AnnotationInput(InputTypeBase): ...@@ -1061,7 +1126,7 @@ class AnnotationInput(InputTypeBase):
'id': index, 'id': index,
'description': option.text, 'description': option.text,
'choice': option.get('choice') 'choice': option.get('choice')
} for (index, option) in enumerate(elements) ] } for (index, option) in enumerate(elements)]
def _validate_options(self): def _validate_options(self):
''' Raises a ValueError if the choice attribute is missing or invalid. ''' ''' Raises a ValueError if the choice attribute is missing or invalid. '''
...@@ -1071,7 +1136,8 @@ class AnnotationInput(InputTypeBase): ...@@ -1071,7 +1136,8 @@ class AnnotationInput(InputTypeBase):
if choice is None: if choice is None:
raise ValueError('Missing required choice attribute.') raise ValueError('Missing required choice attribute.')
elif choice not in valid_choices: elif choice not in valid_choices:
raise ValueError('Invalid choice attribute: {0}. Must be one of: {1}'.format(choice, ', '.join(valid_choices))) raise ValueError('Invalid choice attribute: {0}. Must be one of: {1}'.format(
choice, ', '.join(valid_choices)))
def _unpack(self, json_value): def _unpack(self, json_value):
''' Unpacks the json input state into a dict. ''' ''' Unpacks the json input state into a dict. '''
...@@ -1089,20 +1155,20 @@ class AnnotationInput(InputTypeBase): ...@@ -1089,20 +1155,20 @@ class AnnotationInput(InputTypeBase):
return { return {
'options_value': options_value, 'options_value': options_value,
'has_options_value': len(options_value) > 0, # for convenience 'has_options_value': len(options_value) > 0, # for convenience
'comment_value': comment_value, 'comment_value': comment_value,
} }
def _extra_context(self): def _extra_context(self):
extra_context = { extra_context = {
'title': self.title, 'title': self.title,
'text': self.text, 'text': self.text,
'comment': self.comment, 'comment': self.comment,
'comment_prompt': self.comment_prompt, 'comment_prompt': self.comment_prompt,
'tag_prompt': self.tag_prompt, 'tag_prompt': self.tag_prompt,
'options': self.options, 'options': self.options,
'return_to_annotation': self.return_to_annotation, 'return_to_annotation': self.return_to_annotation,
'debug': self.debug 'debug': self.debug
} }
extra_context.update(self._unpack(self.value)) extra_context.update(self._unpack(self.value))
...@@ -1110,4 +1176,3 @@ class AnnotationInput(InputTypeBase): ...@@ -1110,4 +1176,3 @@ class AnnotationInput(InputTypeBase):
return extra_context return extra_context
registry.register(AnnotationInput) registry.register(AnnotationInput)
...@@ -128,21 +128,25 @@ class LoncapaResponse(object): ...@@ -128,21 +128,25 @@ class LoncapaResponse(object):
for abox in inputfields: for abox in inputfields:
if abox.tag not in self.allowed_inputfields: if abox.tag not in self.allowed_inputfields:
msg = "%s: cannot have input field %s" % (unicode(self), abox.tag) msg = "%s: cannot have input field %s" % (
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>') unicode(self), abox.tag)
msg += "\nSee XML source line %s" % getattr(
xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
if self.max_inputfields and len(inputfields) > self.max_inputfields: if self.max_inputfields and len(inputfields) > self.max_inputfields:
msg = "%s: cannot have more than %s input fields" % ( msg = "%s: cannot have more than %s input fields" % (
unicode(self), self.max_inputfields) unicode(self), self.max_inputfields)
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>') msg += "\nSee XML source line %s" % getattr(
xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
for prop in self.required_attributes: for prop in self.required_attributes:
if not xml.get(prop): if not xml.get(prop):
msg = "Error in problem specification: %s missing required attribute %s" % ( msg = "Error in problem specification: %s missing required attribute %s" % (
unicode(self), prop) unicode(self), prop)
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>') msg += "\nSee XML source line %s" % getattr(
xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
# ordered list of answer_id values for this response # ordered list of answer_id values for this response
...@@ -163,7 +167,8 @@ class LoncapaResponse(object): ...@@ -163,7 +167,8 @@ class LoncapaResponse(object):
for entry in self.inputfields: for entry in self.inputfields:
answer = entry.get('correct_answer') answer = entry.get('correct_answer')
if answer: if answer:
self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context) self.default_answer_map[entry.get(
'id')] = contextualize_text(answer, self.context)
if hasattr(self, 'setup_response'): if hasattr(self, 'setup_response'):
self.setup_response() self.setup_response()
...@@ -211,7 +216,8 @@ class LoncapaResponse(object): ...@@ -211,7 +216,8 @@ class LoncapaResponse(object):
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id. Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
''' '''
new_cmap = self.get_score(student_answers) new_cmap = self.get_score(student_answers)
self.get_hints(convert_files_to_filenames(student_answers), new_cmap, old_cmap) self.get_hints(convert_files_to_filenames(
student_answers), new_cmap, old_cmap)
# log.debug('new_cmap = %s' % new_cmap) # log.debug('new_cmap = %s' % new_cmap)
return new_cmap return new_cmap
...@@ -241,14 +247,17 @@ class LoncapaResponse(object): ...@@ -241,14 +247,17 @@ class LoncapaResponse(object):
# callback procedure to a social hint generation system. # callback procedure to a social hint generation system.
if not hintfn in self.context: if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn msg = 'missing specified hint function %s in script context' % hintfn
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
try: try:
self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap) self.context[hintfn](
self.answer_ids, student_answers, new_cmap, old_cmap)
except Exception as err: except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn) msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise ResponseError(msg) raise ResponseError(msg)
return return
...@@ -270,17 +279,19 @@ class LoncapaResponse(object): ...@@ -270,17 +279,19 @@ class LoncapaResponse(object):
if (self.hint_tag is not None if (self.hint_tag is not None
and hintgroup.find(self.hint_tag) is not None and hintgroup.find(self.hint_tag) is not None
and hasattr(self, 'check_hint_condition')): and hasattr(self, 'check_hint_condition')):
rephints = hintgroup.findall(self.hint_tag) rephints = hintgroup.findall(self.hint_tag)
hints_to_show = self.check_hint_condition(rephints, student_answers) hints_to_show = self.check_hint_condition(
rephints, student_answers)
# can be 'on_request' or 'always' (default) # can be 'on_request' or 'always' (default)
hintmode = hintgroup.get('mode', 'always') hintmode = hintgroup.get('mode', 'always')
for hintpart in hintgroup.findall('hintpart'): for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show: if hintpart.get('on') in hints_to_show:
hint_text = hintpart.find('text').text hint_text = hintpart.find('text').text
# make the hint appear after the last answer box in this response # make the hint appear after the last answer box in this
# response
aid = self.answer_ids[-1] aid = self.answer_ids[-1]
new_cmap.set_hint_and_mode(aid, hint_text, hintmode) new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
log.debug('after hint: new_cmap = %s' % new_cmap) log.debug('after hint: new_cmap = %s' % new_cmap)
...@@ -340,7 +351,6 @@ class LoncapaResponse(object): ...@@ -340,7 +351,6 @@ class LoncapaResponse(object):
response_msg_div = etree.Element('div') response_msg_div = etree.Element('div')
response_msg_div.text = str(response_msg) response_msg_div.text = str(response_msg)
# Set the css class of the message <div> # Set the css class of the message <div>
response_msg_div.set("class", "response_message") response_msg_div.set("class", "response_message")
...@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse): ...@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse):
# until we decide on exactly how to solve this issue. For now, files are # until we decide on exactly how to solve this issue. For now, files are
# manually being compiled to DATA_DIR/js/compiled. # manually being compiled to DATA_DIR/js/compiled.
#latestTimestamp = 0 # latestTimestamp = 0
#basepath = self.system.filestore.root_path + '/js/' # basepath = self.system.filestore.root_path + '/js/'
#for filename in (self.display_dependencies + [self.display]): # for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename # filepath = basepath + filename
# timestamp = os.stat(filepath).st_mtime # timestamp = os.stat(filepath).st_mtime
# if timestamp > latestTimestamp: # if timestamp > latestTimestamp:
# latestTimestamp = timestamp # latestTimestamp = timestamp
# #
#h = hashlib.md5() # h = hashlib.md5()
#h.update(self.answer_id + str(self.display_dependencies)) # h.update(self.answer_id + str(self.display_dependencies))
#compiled_filename = 'compiled/' + h.hexdigest() + '.js' # compiled_filename = 'compiled/' + h.hexdigest() + '.js'
#compiled_filepath = basepath + compiled_filename # compiled_filepath = basepath + compiled_filename
#if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp: # if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
# outfile = open(compiled_filepath, 'w') # outfile = open(compiled_filepath, 'w')
# for filename in (self.display_dependencies + [self.display]): # for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename # filepath = basepath + filename
...@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse): ...@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse):
id=self.xml.get('id'))[0] id=self.xml.get('id'))[0]
self.display_xml = self.xml.xpath('//*[@id=$id]//display', self.display_xml = self.xml.xpath('//*[@id=$id]//display',
id=self.xml.get('id'))[0] id=self.xml.get('id'))[0]
self.xml.remove(self.generator_xml) self.xml.remove(self.generator_xml)
self.xml.remove(self.grader_xml) self.xml.remove(self.grader_xml)
...@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse): ...@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse):
self.display = self.display_xml.get("src") self.display = self.display_xml.get("src")
if self.generator_xml.get("dependencies"): if self.generator_xml.get("dependencies"):
self.generator_dependencies = self.generator_xml.get("dependencies").split() self.generator_dependencies = self.generator_xml.get(
"dependencies").split()
else: else:
self.generator_dependencies = [] self.generator_dependencies = []
if self.grader_xml.get("dependencies"): if self.grader_xml.get("dependencies"):
self.grader_dependencies = self.grader_xml.get("dependencies").split() self.grader_dependencies = self.grader_xml.get(
"dependencies").split()
else: else:
self.grader_dependencies = [] self.grader_dependencies = []
if self.display_xml.get("dependencies"): if self.display_xml.get("dependencies"):
self.display_dependencies = self.display_xml.get("dependencies").split() self.display_dependencies = self.display_xml.get(
"dependencies").split()
else: else:
self.display_dependencies = [] self.display_dependencies = []
...@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse): ...@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse):
return subprocess.check_output(subprocess_args, env=self.get_node_env()) return subprocess.check_output(subprocess_args, env=self.get_node_env())
def generate_problem_state(self): def generate_problem_state(self):
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js' generator_file = os.path.dirname(os.path.normpath(
__file__)) + '/javascript_problem_generator.js'
output = self.call_node([generator_file, output = self.call_node([generator_file,
self.generator, self.generator,
json.dumps(self.generator_dependencies), json.dumps(self.generator_dependencies),
...@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse): ...@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse):
params = {} params = {}
for param in self.xml.xpath('//*[@id=$id]//responseparam', for param in self.xml.xpath('//*[@id=$id]//responseparam',
id=self.xml.get('id')): id=self.xml.get('id')):
raw_param = param.get("value") raw_param = param.get("value")
params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context)) params[param.get("name")] = json.loads(
contextualize_text(raw_param, self.context))
return params return params
def prepare_inputfield(self): def prepare_inputfield(self):
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput', for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
id=self.xml.get('id')): id=self.xml.get('id')):
escapedict = {'"': '&quot;'} escapedict = {'"': '&quot;'}
...@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse): ...@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse):
escapedict) escapedict)
inputfield.set("problem_state", encoded_problem_state) inputfield.set("problem_state", encoded_problem_state)
inputfield.set("display_file", self.display_filename) inputfield.set("display_file", self.display_filename)
inputfield.set("display_class", self.display_class) inputfield.set("display_class", self.display_class)
def get_score(self, student_answers): def get_score(self, student_answers):
...@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse): ...@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse):
if submission is None or submission == '': if submission is None or submission == '':
submission = json.dumps(None) submission = json.dumps(None)
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js' grader_file = os.path.dirname(os.path.normpath(
__file__)) + '/javascript_problem_grader.js'
outputs = self.call_node([grader_file, outputs = self.call_node([grader_file,
self.grader, self.grader,
json.dumps(self.grader_dependencies), json.dumps(self.grader_dependencies),
...@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse): ...@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse):
json.dumps(self.params)]).split('\n') json.dumps(self.params)]).split('\n')
all_correct = json.loads(outputs[0].strip()) all_correct = json.loads(outputs[0].strip())
evaluation = outputs[1].strip() evaluation = outputs[1].strip()
solution = outputs[2].strip() solution = outputs[2].strip()
return (all_correct, evaluation, solution) return (all_correct, evaluation, solution)
def get_answers(self): def get_answers(self):
...@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse): ...@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse):
return {self.answer_id: self.solution} return {self.answer_id: self.solution}
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class ChoiceResponse(LoncapaResponse): class ChoiceResponse(LoncapaResponse):
""" """
This response type is used when the student chooses from a discrete set of This response type is used when the student chooses from a discrete set of
...@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse): ...@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse):
self.assign_choice_names() self.assign_choice_names()
correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]', correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]',
id=self.xml.get('id')) id=self.xml.get('id'))
self.correct_choices = set([choice.get('name') for choice in correct_xml]) self.correct_choices = set([choice.get(
'name') for choice in correct_xml])
def assign_choice_names(self): def assign_choice_names(self):
''' '''
...@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse): ...@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse):
allowed_inputfields = ['choicegroup'] allowed_inputfields = ['choicegroup']
def setup_response(self): def setup_response(self):
# call secondary setup for MultipleChoice questions, to set name attributes # call secondary setup for MultipleChoice questions, to set name
# attributes
self.mc_setup_response() self.mc_setup_response()
# define correct choices (after calling secondary setup) # define correct choices (after calling secondary setup)
...@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse): ...@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
# log.debug('%s: student_answers=%s, correct_choices=%s' % ( # log.debug('%s: student_answers=%s, correct_choices=%s' % (
# unicode(self), student_answers, self.correct_choices)) # unicode(self), student_answers, self.correct_choices))
if (self.answer_id in student_answers if (self.answer_id in student_answers
and student_answers[self.answer_id] in self.correct_choices): and student_answers[self.answer_id] in self.correct_choices):
return CorrectMap(self.answer_id, 'correct') return CorrectMap(self.answer_id, 'correct')
else: else:
return CorrectMap(self.answer_id, 'incorrect') return CorrectMap(self.answer_id, 'incorrect')
...@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse): ...@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse):
return cmap return cmap
def get_answers(self): def get_answers(self):
amap = dict([(af.get('id'), contextualize_text(af.get('correct'), self.context)) for af in self.answer_fields]) amap = dict([(af.get('id'), contextualize_text(af.get(
'correct'), self.context)) for af in self.answer_fields])
# log.debug('%s: expected answers=%s' % (unicode(self),amap)) # log.debug('%s: expected answers=%s' % (unicode(self),amap))
return amap return amap
...@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse): ...@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse):
context = self.context context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
try: try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', self.tolerance_xml = xml.xpath(
id=xml.get('id'))[0] '//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context) self.tolerance = contextualize_text(self.tolerance_xml, context)
except Exception: except Exception:
self.tolerance = '0' self.tolerance = '0'
...@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse): ...@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse):
try: try:
correct_ans = complex(self.correct_answer) correct_ans = complex(self.correct_answer)
except ValueError: except ValueError:
log.debug("Content error--answer '{0}' is not a valid complex number".format(self.correct_answer)) log.debug("Content error--answer '{0}' is not a valid complex number".format(
raise StudentInputError("There was a problem with the staff answer to this problem") self.correct_answer))
raise StudentInputError(
"There was a problem with the staff answer to this problem")
try: try:
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer), correct = compare_with_tolerance(
correct_ans, self.tolerance) evaluator(dict(), dict(), student_answer),
correct_ans, self.tolerance)
# We should catch this explicitly. # We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable: # I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm # But we'd need to confirm
except: except:
# Use the traceback-preserving version of re-raising with a different type # Use the traceback-preserving version of re-raising with a
# different type
import sys import sys
type, value, traceback = sys.exc_info() type, value, traceback = sys.exc_info()
...@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse): ...@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse):
max_inputfields = 1 max_inputfields = 1
def setup_response(self): def setup_response(self):
self.correct_answer = contextualize_text(self.xml.get('answer'), self.context).strip() self.correct_answer = contextualize_text(
self.xml.get('answer'), self.context).strip()
def get_score(self, student_answers): def get_score(self, student_answers):
'''Grade a string response ''' '''Grade a string response '''
...@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse): ...@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse):
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect') return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
def check_string(self, expected, given): def check_string(self, expected, given):
if self.xml.get('type') == 'ci': return given.lower() == expected.lower() if self.xml.get('type') == 'ci':
return given.lower() == expected.lower()
return given == expected return given == expected
def check_hint_condition(self, hxml_set, student_answers): def check_hint_condition(self, hxml_set, student_answers):
...@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse): ...@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse):
hints_to_show = [] hints_to_show = []
for hxml in hxml_set: for hxml in hxml_set:
name = hxml.get('name') name = hxml.get('name')
correct_answer = contextualize_text(hxml.get('answer'), self.context).strip() correct_answer = contextualize_text(
if self.check_string(correct_answer, given): hints_to_show.append(name) hxml.get('answer'), self.context).strip()
if self.check_string(correct_answer, given):
hints_to_show.append(name)
log.debug('hints_to_show = %s' % hints_to_show) log.debug('hints_to_show = %s' % hints_to_show)
return hints_to_show return hints_to_show
...@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse): ...@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse):
correct[0] ='incorrect' correct[0] ='incorrect'
</answer> </answer>
</customresponse>"""}, </customresponse>"""},
{'snippet': """<script type="loncapa/python"><![CDATA[ {'snippet': """<script type="loncapa/python"><![CDATA[
def sympy_check2(): def sympy_check2():
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','&lt;')) messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','&lt;'))
...@@ -907,15 +932,16 @@ def sympy_check2(): ...@@ -907,15 +932,16 @@ def sympy_check2():
response_tag = 'customresponse' response_tag = 'customresponse'
allowed_inputfields = ['textline', 'textbox', 'crystallography', allowed_inputfields = ['textline', 'textbox', 'crystallography',
'chemicalequationinput', 'vsepr_input', 'chemicalequationinput', 'vsepr_input',
'drag_and_drop_input', 'editamoleculeinput', 'drag_and_drop_input', 'editamoleculeinput',
'designprotein2dinput', 'editageneinput', 'designprotein2dinput', 'editageneinput',
'annotationinput'] 'annotationinput']
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
# if <customresponse> has an "expect" (or "answer") attribute then save that # if <customresponse> has an "expect" (or "answer") attribute then save
# that
self.expect = xml.get('expect') or xml.get('answer') self.expect = xml.get('expect') or xml.get('answer')
self.myid = xml.get('id') self.myid = xml.get('id')
...@@ -939,7 +965,8 @@ def sympy_check2(): ...@@ -939,7 +965,8 @@ def sympy_check2():
if cfn in self.context: if cfn in self.context:
self.code = self.context[cfn] self.code = self.context[cfn]
else: else:
msg = "%s: can't find cfn %s in context" % (unicode(self), cfn) msg = "%s: can't find cfn %s in context" % (
unicode(self), cfn)
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline',
'<unavailable>') '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
...@@ -952,7 +979,8 @@ def sympy_check2(): ...@@ -952,7 +979,8 @@ def sympy_check2():
else: else:
answer_src = answer.get('src') answer_src = answer.get('src')
if answer_src is not None: if answer_src is not None:
self.code = self.system.filesystem.open('src/' + answer_src).read() self.code = self.system.filesystem.open(
'src/' + answer_src).read()
else: else:
self.code = answer.text self.code = answer.text
...@@ -1032,7 +1060,7 @@ def sympy_check2(): ...@@ -1032,7 +1060,7 @@ def sympy_check2():
# any options to be passed to the cfn # any options to be passed to the cfn
'options': self.xml.get('options'), 'options': self.xml.get('options'),
'testdat': 'hello world', 'testdat': 'hello world',
}) })
# pass self.system.debug to cfn # pass self.system.debug to cfn
self.context['debug'] = self.system.DEBUG self.context['debug'] = self.system.DEBUG
...@@ -1049,7 +1077,8 @@ def sympy_check2(): ...@@ -1049,7 +1077,8 @@ def sympy_check2():
print "context = ", self.context print "context = ", self.context
print traceback.format_exc() print traceback.format_exc()
# Notify student # Notify student
raise StudentInputError("Error: Problem could not be evaluated with your input") raise StudentInputError(
"Error: Problem could not be evaluated with your input")
else: else:
# self.code is not a string; assume its a function # self.code is not a string; assume its a function
...@@ -1058,18 +1087,22 @@ def sympy_check2(): ...@@ -1058,18 +1087,22 @@ def sympy_check2():
ret = None ret = None
log.debug(" submission = %s" % submission) log.debug(" submission = %s" % submission)
try: try:
answer_given = submission[0] if (len(idset) == 1) else submission answer_given = submission[0] if (
len(idset) == 1) else submission
# handle variable number of arguments in check function, for backwards compatibility # handle variable number of arguments in check function, for backwards compatibility
# with various Tutor2 check functions # with various Tutor2 check functions
args = [self.expect, answer_given, student_answers, self.answer_ids[0]] args = [self.expect, answer_given,
student_answers, self.answer_ids[0]]
argspec = inspect.getargspec(fn) argspec = inspect.getargspec(fn)
nargs = len(argspec.args) - len(argspec.defaults or []) nargs = len(argspec.args) - len(argspec.defaults or [])
kwargs = {} kwargs = {}
for argname in argspec.args[nargs:]: for argname in argspec.args[nargs:]:
kwargs[argname] = self.context[argname] if argname in self.context else None kwargs[argname] = self.context[
argname] if argname in self.context else None
log.debug('[customresponse] answer_given=%s' % answer_given) log.debug('[customresponse] answer_given=%s' % answer_given)
log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs, args, kwargs)) log.debug('nargs=%d, args=%s, kwargs=%s' % (
nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs) ret = fn(*args[:nargs], **kwargs)
except Exception as err: except Exception as err:
...@@ -1077,7 +1110,8 @@ def sympy_check2(): ...@@ -1077,7 +1110,8 @@ def sympy_check2():
# print "context = ",self.context # print "context = ",self.context
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise Exception("oops in customresponse (cfn) error %s" % err) raise Exception("oops in customresponse (cfn) error %s" % err)
log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret) log.debug(
"[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret) == dict: if type(ret) == dict:
...@@ -1086,7 +1120,8 @@ def sympy_check2(): ...@@ -1086,7 +1120,8 @@ def sympy_check2():
# If there are multiple inputs, they all get marked # If there are multiple inputs, they all get marked
# to the same correct/incorrect value # to the same correct/incorrect value
if 'ok' in ret: if 'ok' in ret:
correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset) correct = ['correct'] * len(idset) if ret[
'ok'] else ['incorrect'] * len(idset)
msg = ret.get('msg', None) msg = ret.get('msg', None)
msg = self.clean_message_html(msg) msg = self.clean_message_html(msg)
...@@ -1097,7 +1132,6 @@ def sympy_check2(): ...@@ -1097,7 +1132,6 @@ def sympy_check2():
else: else:
messages[0] = msg messages[0] = msg
# Another kind of dictionary the check function can return has # Another kind of dictionary the check function can return has
# the form: # the form:
# {'overall_message': STRING, # {'overall_message': STRING,
...@@ -1113,21 +1147,25 @@ def sympy_check2(): ...@@ -1113,21 +1147,25 @@ def sympy_check2():
correct = [] correct = []
messages = [] messages = []
for input_dict in input_list: for input_dict in input_list:
correct.append('correct' if input_dict['ok'] else 'incorrect') correct.append('correct' if input_dict[
msg = self.clean_message_html(input_dict['msg']) if 'msg' in input_dict else None 'ok'] else 'incorrect')
msg = self.clean_message_html(input_dict[
'msg']) if 'msg' in input_dict else None
messages.append(msg) messages.append(msg)
# Otherwise, we do not recognize the dictionary # Otherwise, we do not recognize the dictionary
# Raise an exception # Raise an exception
else: else:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise Exception("CustomResponse: check function returned an invalid dict") raise Exception(
"CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value, # The check function can return a boolean value,
# indicating whether all inputs should be marked # indicating whether all inputs should be marked
# correct or incorrect # correct or incorrect
else: else:
correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset) correct = ['correct'] * len(
idset) if ret else ['incorrect'] * len(idset)
# build map giving "correct"ness of the answer(s) # build map giving "correct"ness of the answer(s)
correct_map = CorrectMap() correct_map = CorrectMap()
...@@ -1136,7 +1174,8 @@ def sympy_check2(): ...@@ -1136,7 +1174,8 @@ def sympy_check2():
correct_map.set_overall_message(overall_message) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
npoints = self.maxpoints[idset[k]] if correct[k] == 'correct' else 0 npoints = self.maxpoints[idset[
k]] if correct[k] == 'correct' else 0
correct_map.set(idset[k], correct[k], msg=messages[k], correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints) npoints=npoints)
return correct_map return correct_map
...@@ -1242,7 +1281,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1242,7 +1281,7 @@ class CodeResponse(LoncapaResponse):
""" """
response_tag = 'coderesponse' response_tag = 'coderesponse'
allowed_inputfields = ['textbox', 'filesubmission'] allowed_inputfields = ['textbox', 'filesubmission', 'matlabinput']
max_inputfields = 1 max_inputfields = 1
def setup_response(self): def setup_response(self):
...@@ -1263,7 +1302,8 @@ class CodeResponse(LoncapaResponse): ...@@ -1263,7 +1302,8 @@ class CodeResponse(LoncapaResponse):
self.queue_name = xml.get('queuename', default_queuename) self.queue_name = xml.get('queuename', default_queuename)
# VS[compat]: # VS[compat]:
# Check if XML uses the ExternalResponse format or the generic CodeResponse format # Check if XML uses the ExternalResponse format or the generic
# CodeResponse format
codeparam = self.xml.find('codeparam') codeparam = self.xml.find('codeparam')
if codeparam is None: if codeparam is None:
self._parse_externalresponse_xml() self._parse_externalresponse_xml()
...@@ -1277,12 +1317,14 @@ class CodeResponse(LoncapaResponse): ...@@ -1277,12 +1317,14 @@ class CodeResponse(LoncapaResponse):
self.answer (an answer to display to the student in the LMS) self.answer (an answer to display to the student in the LMS)
self.payload self.payload
''' '''
# Note that CodeResponse is agnostic to the specific contents of grader_payload # Note that CodeResponse is agnostic to the specific contents of
# grader_payload
grader_payload = codeparam.find('grader_payload') grader_payload = codeparam.find('grader_payload')
grader_payload = grader_payload.text if grader_payload is not None else '' grader_payload = grader_payload.text if grader_payload is not None else ''
self.payload = {'grader_payload': grader_payload} self.payload = {'grader_payload': grader_payload}
self.initial_display = find_with_default(codeparam, 'initial_display', '') self.initial_display = find_with_default(
codeparam, 'initial_display', '')
self.answer = find_with_default(codeparam, 'answer_display', self.answer = find_with_default(codeparam, 'answer_display',
'No answer provided.') 'No answer provided.')
...@@ -1304,8 +1346,10 @@ class CodeResponse(LoncapaResponse): ...@@ -1304,8 +1346,10 @@ class CodeResponse(LoncapaResponse):
else: # no <answer> stanza; get code from <script> else: # no <answer> stanza; get code from <script>
code = self.context['script_code'] code = self.context['script_code']
if not code: if not code:
msg = '%s: Missing answer script code for coderesponse' % unicode(self) msg = '%s: Missing answer script code for coderesponse' % unicode(
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') self)
msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
tests = self.xml.get('tests') tests = self.xml.get('tests')
...@@ -1320,7 +1364,8 @@ class CodeResponse(LoncapaResponse): ...@@ -1320,7 +1364,8 @@ class CodeResponse(LoncapaResponse):
try: try:
exec(code, penv, penv) exec(code, penv, penv)
except Exception as err: except Exception as err:
log.error('Error in CodeResponse %s: Error in problem reference code' % err) log.error(
'Error in CodeResponse %s: Error in problem reference code' % err)
raise Exception(err) raise Exception(err)
try: try:
self.answer = penv['answer'] self.answer = penv['answer']
...@@ -1333,7 +1378,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1333,7 +1378,7 @@ class CodeResponse(LoncapaResponse):
# Finally, make the ExternalResponse input XML format conform to the generic # Finally, make the ExternalResponse input XML format conform to the generic
# exteral grader interface # exteral grader interface
# The XML tagging of grader_payload is pyxserver-specific # The XML tagging of grader_payload is pyxserver-specific
grader_payload = '<pyxserver>' grader_payload = '<pyxserver>'
grader_payload += '<tests>' + tests + '</tests>\n' grader_payload += '<tests>' + tests + '</tests>\n'
grader_payload += '<processor>' + code + '</processor>' grader_payload += '<processor>' + code + '</processor>'
grader_payload += '</pyxserver>' grader_payload += '</pyxserver>'
...@@ -1346,14 +1391,14 @@ class CodeResponse(LoncapaResponse): ...@@ -1346,14 +1391,14 @@ class CodeResponse(LoncapaResponse):
except Exception as err: except Exception as err:
log.error('Error in CodeResponse %s: cannot get student answer for %s;' log.error('Error in CodeResponse %s: cannot get student answer for %s;'
' student_answers=%s' % ' student_answers=%s' %
(err, self.answer_id, convert_files_to_filenames(student_answers))) (err, self.answer_id, convert_files_to_filenames(student_answers)))
raise Exception(err) raise Exception(err)
# We do not support xqueue within Studio. # We do not support xqueue within Studio.
if self.system.xqueue is None: if self.system.xqueue is None:
cmap = CorrectMap() cmap = CorrectMap()
cmap.set(self.answer_id, queuestate=None, cmap.set(self.answer_id, queuestate=None,
msg='Error checking problem: no external queueing server is configured.') msg='Error checking problem: no external queueing server is configured.')
return cmap return cmap
# Prepare xqueue request # Prepare xqueue request
...@@ -1368,9 +1413,10 @@ class CodeResponse(LoncapaResponse): ...@@ -1368,9 +1413,10 @@ class CodeResponse(LoncapaResponse):
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
anonymous_student_id + anonymous_student_id +
self.answer_id) self.answer_id)
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], xheader = xqueue_interface.make_xheader(
lms_key=queuekey, lms_callback_url=self.system.xqueue['callback_url'],
queue_name=self.queue_name) lms_key=queuekey,
queue_name=self.queue_name)
# Generate body # Generate body
if is_list_of_files(submission): if is_list_of_files(submission):
...@@ -1381,13 +1427,16 @@ class CodeResponse(LoncapaResponse): ...@@ -1381,13 +1427,16 @@ class CodeResponse(LoncapaResponse):
contents = self.payload.copy() contents = self.payload.copy()
# Metadata related to the student submission revealed to the external grader # Metadata related to the student submission revealed to the external
# grader
student_info = {'anonymous_student_id': anonymous_student_id, student_info = {'anonymous_student_id': anonymous_student_id,
'submission_time': qtime, 'submission_time': qtime,
} }
contents.update({'student_info': json.dumps(student_info)}) contents.update({'student_info': json.dumps(student_info)})
# Submit request. When successful, 'msg' is the prior length of the queue # Submit request. When successful, 'msg' is the prior length of the
# queue
if is_list_of_files(submission): if is_list_of_files(submission):
# TODO: Is there any information we want to send here? # TODO: Is there any information we want to send here?
contents.update({'student_response': ''}) contents.update({'student_response': ''})
...@@ -1415,13 +1464,15 @@ class CodeResponse(LoncapaResponse): ...@@ -1415,13 +1464,15 @@ class CodeResponse(LoncapaResponse):
# 2) Frontend: correctness='incomplete' eventually trickles down # 2) Frontend: correctness='incomplete' eventually trickles down
# through inputtypes.textbox and .filesubmission to inform the # through inputtypes.textbox and .filesubmission to inform the
# browser to poll the LMS # browser to poll the LMS
cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) cmap.set(self.answer_id, queuestate=queuestate,
correctness='incomplete', msg=msg)
return cmap return cmap
def update_score(self, score_msg, oldcmap, queuekey): def update_score(self, score_msg, oldcmap, queuekey):
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) (valid_score_msg, correct, points,
msg) = self._parse_score_msg(score_msg)
if not valid_score_msg: if not valid_score_msg:
oldcmap.set(self.answer_id, oldcmap.set(self.answer_id,
msg='Invalid grader reply. Please contact the course staff.') msg='Invalid grader reply. Please contact the course staff.')
...@@ -1433,14 +1484,16 @@ class CodeResponse(LoncapaResponse): ...@@ -1433,14 +1484,16 @@ class CodeResponse(LoncapaResponse):
self.context['correct'] = correctness self.context['correct'] = correctness
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
# does not match, we keep waiting for the score_msg whose key actually matches # does not match, we keep waiting for the score_msg whose key actually
# matches
if oldcmap.is_right_queuekey(self.answer_id, queuekey): if oldcmap.is_right_queuekey(self.answer_id, queuekey):
# Sanity check on returned points # Sanity check on returned points
if points < 0: if points < 0:
points = 0 points = 0
# Queuestate is consumed # Queuestate is consumed
oldcmap.set(self.answer_id, npoints=points, correctness=correctness, oldcmap.set(
msg=msg.replace('&nbsp;', '&#160;'), queuestate=None) self.answer_id, npoints=points, correctness=correctness,
msg=msg.replace('&nbsp;', '&#160;'), queuestate=None)
else: else:
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' %
(queuekey, self.answer_id)) (queuekey, self.answer_id))
...@@ -1560,15 +1613,18 @@ main() ...@@ -1560,15 +1613,18 @@ main()
if answer is not None: if answer is not None:
answer_src = answer.get('src') answer_src = answer.get('src')
if answer_src is not None: if answer_src is not None:
self.code = self.system.filesystem.open('src/' + answer_src).read() self.code = self.system.filesystem.open(
'src/' + answer_src).read()
else: else:
self.code = answer.text self.code = answer.text
else: else:
# no <answer> stanza; get code from <script> # no <answer> stanza; get code from <script>
self.code = self.context['script_code'] self.code = self.context['script_code']
if not self.code: if not self.code:
msg = '%s: Missing answer script code for externalresponse' % unicode(self) msg = '%s: Missing answer script code for externalresponse' % unicode(
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') self)
msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg) raise LoncapaProblemError(msg)
self.tests = xml.get('tests') self.tests = xml.get('tests')
...@@ -1591,10 +1647,12 @@ main() ...@@ -1591,10 +1647,12 @@ main()
payload.update(extra_payload) payload.update(extra_payload)
try: try:
# call external server. TODO: synchronous call, can block for a long time # call external server. TODO: synchronous call, can block for a
# long time
r = requests.post(self.url, data=payload) r = requests.post(self.url, data=payload)
except Exception as err: except Exception as err:
msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url) msg = 'Error %s - cannot connect to external server url=%s' % (
err, self.url)
log.error(msg) log.error(msg)
raise Exception(msg) raise Exception(msg)
...@@ -1602,13 +1660,15 @@ main() ...@@ -1602,13 +1660,15 @@ main()
log.info('response = %s' % r.text) log.info('response = %s' % r.text)
if (not r.text) or (not r.text.strip()): if (not r.text) or (not r.text.strip()):
raise Exception('Error: no response from external server url=%s' % self.url) raise Exception(
'Error: no response from external server url=%s' % self.url)
try: try:
# response is XML; parse it # response is XML; parse it
rxml = etree.fromstring(r.text) rxml = etree.fromstring(r.text)
except Exception as err: except Exception as err:
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text) msg = 'Error %s - cannot parse response from external server r.text=%s' % (
err, r.text)
log.error(msg) log.error(msg)
raise Exception(msg) raise Exception(msg)
...@@ -1633,7 +1693,8 @@ main() ...@@ -1633,7 +1693,8 @@ main()
except Exception as err: except Exception as err:
log.error('Error %s' % err) log.error('Error %s' % err)
if self.system.DEBUG: if self.system.DEBUG:
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset)))) cmap.set_dict(dict(zip(sorted(
self.answer_ids), ['incorrect'] * len(idset))))
cmap.set_property( cmap.set_property(
self.answer_ids[0], 'msg', self.answer_ids[0], 'msg',
'<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;')) '<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;'))
...@@ -1650,7 +1711,8 @@ main() ...@@ -1650,7 +1711,8 @@ main()
# create CorrectMap # create CorrectMap
for key in idset: for key in idset:
idx = idset.index(key) idx = idset.index(key)
msg = rxml.find('message').text.replace('&nbsp;', '&#160;') if idx == 0 else None msg = rxml.find('message').text.replace(
'&nbsp;', '&#160;') if idx == 0 else None
cmap.set(key, self.context['correct'][idx], msg=msg) cmap.set(key, self.context['correct'][idx], msg=msg)
return cmap return cmap
...@@ -1665,7 +1727,8 @@ main() ...@@ -1665,7 +1727,8 @@ main()
except Exception as err: except Exception as err:
log.error('Error %s' % err) log.error('Error %s' % err)
if self.system.DEBUG: if self.system.DEBUG:
msg = '<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;') msg = '<span class="inline-error">%s</span>' % str(
err).replace('<', '&lt;')
exans = [''] * len(self.answer_ids) exans = [''] * len(self.answer_ids)
exans[0] = msg exans[0] = msg
...@@ -1712,8 +1775,9 @@ class FormulaResponse(LoncapaResponse): ...@@ -1712,8 +1775,9 @@ class FormulaResponse(LoncapaResponse):
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context) self.samples = contextualize_text(xml.get('samples'), context)
try: try:
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', self.tolerance_xml = xml.xpath(
id=xml.get('id'))[0] '//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context) self.tolerance = contextualize_text(self.tolerance_xml, context)
except Exception: except Exception:
self.tolerance = '0.00001' self.tolerance = '0.00001'
...@@ -1735,14 +1799,15 @@ class FormulaResponse(LoncapaResponse): ...@@ -1735,14 +1799,15 @@ class FormulaResponse(LoncapaResponse):
def get_score(self, student_answers): def get_score(self, student_answers):
given = student_answers[self.answer_id] given = student_answers[self.answer_id]
correctness = self.check_formula(self.correct_answer, given, self.samples) correctness = self.check_formula(
self.correct_answer, given, self.samples)
return CorrectMap(self.answer_id, correctness) return CorrectMap(self.answer_id, correctness)
def check_formula(self, expected, given, samples): def check_formula(self, expected, given, samples):
variables = samples.split('@')[0].split(',') variables = samples.split('@')[0].split(',')
numsamples = int(samples.split('@')[1].split('#')[1]) numsamples = int(samples.split('@')[1].split('#')[1])
sranges = zip(*map(lambda x: map(float, x.split(",")), sranges = zip(*map(lambda x: map(float, x.split(",")),
samples.split('@')[1].split('#')[0].split(':'))) samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges)) ranges = dict(zip(variables, sranges))
for i in range(numsamples): for i in range(numsamples):
...@@ -1753,22 +1818,26 @@ class FormulaResponse(LoncapaResponse): ...@@ -1753,22 +1818,26 @@ class FormulaResponse(LoncapaResponse):
value = random.uniform(*ranges[var]) value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value instructor_variables[str(var)] = value
student_variables[str(var)] = value student_variables[str(var)] = value
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected)) # log.debug('formula: instructor_vars=%s, expected=%s' %
# (instructor_variables,expected))
instructor_result = evaluator(instructor_variables, dict(), instructor_result = evaluator(instructor_variables, dict(),
expected, cs=self.case_sensitive) expected, cs=self.case_sensitive)
try: try:
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given)) # log.debug('formula: student_vars=%s, given=%s' %
# (student_variables,given))
student_result = evaluator(student_variables, student_result = evaluator(student_variables,
dict(), dict(),
given, given,
cs=self.case_sensitive) cs=self.case_sensitive)
except UndefinedVariable as uv: except UndefinedVariable as uv:
log.debug('formularesponse: undefined variable in given=%s' % given) log.debug(
raise StudentInputError("Invalid input: " + uv.message + " not permitted in answer") 'formularesponse: undefined variable in given=%s' % given)
raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer")
except Exception as err: except Exception as err:
#traceback.print_exc() # traceback.print_exc()
log.debug('formularesponse: error %s in formula' % err) log.debug('formularesponse: error %s in formula' % err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %\ raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given)) cgi.escape(given))
if numpy.isnan(student_result) or numpy.isinf(student_result): if numpy.isnan(student_result) or numpy.isinf(student_result):
return "incorrect" return "incorrect"
...@@ -1792,9 +1861,11 @@ class FormulaResponse(LoncapaResponse): ...@@ -1792,9 +1861,11 @@ class FormulaResponse(LoncapaResponse):
for hxml in hxml_set: for hxml in hxml_set:
samples = hxml.get('samples') samples = hxml.get('samples')
name = hxml.get('name') name = hxml.get('name')
correct_answer = contextualize_text(hxml.get('answer'), self.context) correct_answer = contextualize_text(
hxml.get('answer'), self.context)
try: try:
correctness = self.check_formula(correct_answer, given, samples) correctness = self.check_formula(
correct_answer, given, samples)
except Exception: except Exception:
correctness = 'incorrect' correctness = 'incorrect'
if correctness == 'correct': if correctness == 'correct':
...@@ -1825,11 +1896,13 @@ class SchematicResponse(LoncapaResponse): ...@@ -1825,11 +1896,13 @@ class SchematicResponse(LoncapaResponse):
def get_score(self, student_answers): def get_score(self, student_answers):
from capa_problem import global_context from capa_problem import global_context
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
exec self.code in global_context, self.context exec self.code in global_context, self.context
cmap = CorrectMap() cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct']))) cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct'])))
return cmap return cmap
def get_answers(self): def get_answers(self):
...@@ -1891,12 +1964,14 @@ class ImageResponse(LoncapaResponse): ...@@ -1891,12 +1964,14 @@ class ImageResponse(LoncapaResponse):
expectedset = self.get_answers() expectedset = self.get_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput> for aid in self.answer_ids: # loop through IDs of <imageinput>
# fields in our stanza # fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]' given = student_answers[
aid] # this should be a string of the form '[x,y]'
correct_map.set(aid, 'incorrect') correct_map.set(aid, 'incorrect')
if not given: # No answer to parse. Mark as incorrect and move on if not given: # No answer to parse. Mark as incorrect and move on
continue continue
# parse given answer # parse given answer
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) m = re.match(
'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m: if not m:
raise Exception('[capamodule.capa.responsetypes.imageinput] ' raise Exception('[capamodule.capa.responsetypes.imageinput] '
'error grading %s (input=%s)' % (aid, given)) 'error grading %s (input=%s)' % (aid, given))
...@@ -1904,20 +1979,24 @@ class ImageResponse(LoncapaResponse): ...@@ -1904,20 +1979,24 @@ class ImageResponse(LoncapaResponse):
rectangles, regions = expectedset rectangles, regions = expectedset
if rectangles[aid]: # rectangles part - for backward compatibility if rectangles[aid]: # rectangles part - for backward compatibility
# Check whether given point lies in any of the solution rectangles # Check whether given point lies in any of the solution
# rectangles
solution_rectangles = rectangles[aid].split(';') solution_rectangles = rectangles[aid].split(';')
for solution_rectangle in solution_rectangles: for solution_rectangle in solution_rectangles:
# parse expected answer # parse expected answer
# TODO: Compile regexp on file load # TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', m = re.match(
solution_rectangle.strip().replace(' ', '')) '[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
solution_rectangle.strip().replace(' ', ''))
if not m: if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % ( msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True)) etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg) raise Exception(
'[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()] (llx, lly, urx, ury) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle # answer is correct if (x,y) is within the specified
# rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury): if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map.set(aid, 'correct') correct_map.set(aid, 'correct')
break break
...@@ -1938,10 +2017,13 @@ class ImageResponse(LoncapaResponse): ...@@ -1938,10 +2017,13 @@ class ImageResponse(LoncapaResponse):
return correct_map return correct_map
def get_answers(self): def get_answers(self):
return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]), return (
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) dict([(ie.get('id'), ie.get(
'rectangle')) for ie in self.ielements]),
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class AnnotationResponse(LoncapaResponse): class AnnotationResponse(LoncapaResponse):
''' '''
Checking of annotation responses. Checking of annotation responses.
...@@ -1952,7 +2034,8 @@ class AnnotationResponse(LoncapaResponse): ...@@ -1952,7 +2034,8 @@ class AnnotationResponse(LoncapaResponse):
response_tag = 'annotationresponse' response_tag = 'annotationresponse'
allowed_inputfields = ['annotationinput'] allowed_inputfields = ['annotationinput']
max_inputfields = 1 max_inputfields = 1
default_scoring = {'incorrect': 0, 'partially-correct': 1, 'correct': 2 } default_scoring = {'incorrect': 0, 'partially-correct': 1, 'correct': 2}
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
self.scoring_map = self._get_scoring_map() self.scoring_map = self._get_scoring_map()
...@@ -1966,7 +2049,8 @@ class AnnotationResponse(LoncapaResponse): ...@@ -1966,7 +2049,8 @@ class AnnotationResponse(LoncapaResponse):
student_option = self._get_submitted_option_id(student_answer) student_option = self._get_submitted_option_id(student_answer)
scoring = self.scoring_map[self.answer_id] scoring = self.scoring_map[self.answer_id]
is_valid = student_option is not None and student_option in scoring.keys() is_valid = student_option is not None and student_option in scoring.keys(
)
(correctness, points) = ('incorrect', None) (correctness, points) = ('incorrect', None)
if is_valid: if is_valid:
...@@ -1981,7 +2065,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -1981,7 +2065,7 @@ class AnnotationResponse(LoncapaResponse):
def _get_scoring_map(self): def _get_scoring_map(self):
''' Returns a dict of option->scoring for each input. ''' ''' Returns a dict of option->scoring for each input. '''
scoring = self.default_scoring scoring = self.default_scoring
choices = dict([(choice,choice) for choice in scoring]) choices = dict([(choice, choice) for choice in scoring])
scoring_map = {} scoring_map = {}
for inputfield in self.inputfields: for inputfield in self.inputfields:
...@@ -1998,9 +2082,11 @@ class AnnotationResponse(LoncapaResponse): ...@@ -1998,9 +2082,11 @@ class AnnotationResponse(LoncapaResponse):
''' Returns a dict of answers for each input.''' ''' Returns a dict of answers for each input.'''
answer_map = {} answer_map = {}
for inputfield in self.inputfields: for inputfield in self.inputfields:
correct_option = self._find_option_with_choice(inputfield, 'correct') correct_option = self._find_option_with_choice(
inputfield, 'correct')
if correct_option is not None: if correct_option is not None:
answer_map[inputfield.get('id')] = correct_option.get('description') answer_map[inputfield.get(
'id')] = correct_option.get('description')
return answer_map return answer_map
def _get_max_points(self): def _get_max_points(self):
...@@ -2016,7 +2102,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2016,7 +2102,7 @@ class AnnotationResponse(LoncapaResponse):
'id': index, 'id': index,
'description': option.text, 'description': option.text,
'choice': option.get('choice') 'choice': option.get('choice')
} for (index, option) in enumerate(elements) ] } for (index, option) in enumerate(elements)]
def _find_option_with_choice(self, inputfield, choice): def _find_option_with_choice(self, inputfield, choice):
''' Returns the option with the given choice value, otherwise None. ''' ''' Returns the option with the given choice value, otherwise None. '''
......
<section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}"
% if hidden:
style="display:none;"
% endif
>${value|h}</textarea>
<div class="grader-status">
% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span>
% elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<p class="debug">${status}</p>
</div>
<span id="answer_${id}"></span>
<div class="external-grader-message">
${msg|n}
</div>
<div class="plot-button">
<input type="button" name="plot-button" value="Plot" />
</div>
<script>
// Note: We need to make the area follow the CodeMirror for this to work.
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"), {
% if linenumbers == 'true':
lineNumbers: true,
% endif
mode: "${mode}",
matchBrackets: true,
lineWrapping: true,
indentUnit: "${tabsize}",
tabSize: "${tabsize}",
indentWithTabs: false,
extraKeys: {
"Tab": function(cm) {
cm.replaceSelection("${' '*tabsize}", "end");
}
},
smartIndent: false
});
$("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))});
});
</script>
</section>
...@@ -70,6 +70,7 @@ class @Problem ...@@ -70,6 +70,7 @@ class @Problem
@bind() @bind()
@num_queued_items = @new_queued_items.length @num_queued_items = @new_queued_items.length
@updateProgress response
if @num_queued_items == 0 if @num_queued_items == 0
delete window.queuePollerID delete window.queuePollerID
else else
......
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