Merge pull request #1703 from MITx/feature/diana/matlab-input
Matlab Input Type
This commit is contained in:
@@ -16,7 +16,6 @@ This is used by capa_module.
|
||||
from __future__ import division
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import numpy
|
||||
@@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape
|
||||
from copy import deepcopy
|
||||
|
||||
import chem
|
||||
import chem.chemcalc
|
||||
import chem.chemtools
|
||||
import chem.miller
|
||||
import verifiers
|
||||
import verifiers.draganddrop
|
||||
@@ -70,9 +67,6 @@ global_context = {'random': random,
|
||||
'scipy': scipy,
|
||||
'calc': calc,
|
||||
'eia': eia,
|
||||
'chemcalc': chem.chemcalc,
|
||||
'chemtools': chem.chemtools,
|
||||
'miller': chem.miller,
|
||||
'draganddrop': verifiers.draganddrop}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
@@ -97,8 +91,13 @@ class LoncapaProblem(object):
|
||||
|
||||
- problem_text (string): xml defining the problem
|
||||
- id (string): identifier for this problem; often a filename (no spaces)
|
||||
- state (dict): student state
|
||||
- seed (int): random number generator seed (int)
|
||||
- seed (int): random number generator seed (int)
|
||||
- state (dict): containing the following keys:
|
||||
- 'seed' - (int) random number generator seed
|
||||
- 'student_answers' - (dict) maps input id to the stored answer for that input
|
||||
- 'correct_map' (CorrectMap) a map of each input to their 'correctness'
|
||||
- 'done' - (bool) indicates whether or not this problem is considered done
|
||||
- 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input
|
||||
- system (ModuleSystem): ModuleSystem instance which provides OS,
|
||||
rendering, and user context
|
||||
|
||||
@@ -110,21 +109,23 @@ class LoncapaProblem(object):
|
||||
self.system = system
|
||||
if self.system is None:
|
||||
raise Exception()
|
||||
self.seed = seed
|
||||
|
||||
if state:
|
||||
if 'seed' in state:
|
||||
self.seed = state['seed']
|
||||
if 'student_answers' in state:
|
||||
self.student_answers = state['student_answers']
|
||||
if 'correct_map' in state:
|
||||
self.correct_map.set_dict(state['correct_map'])
|
||||
if 'done' in state:
|
||||
self.done = state['done']
|
||||
state = state if state else {}
|
||||
|
||||
# Set seed according to the following priority:
|
||||
# 1. Contained in problem's state
|
||||
# 2. Passed into capa_problem via constructor
|
||||
# 3. Assign from the OS's random number generator
|
||||
self.seed = state.get('seed', seed)
|
||||
if self.seed is None:
|
||||
self.seed = struct.unpack('i', os.urandom(4))
|
||||
self.student_answers = state.get('student_answers', {})
|
||||
if 'correct_map' in state:
|
||||
self.correct_map.set_dict(state['correct_map'])
|
||||
self.done = state.get('done', False)
|
||||
self.input_state = state.get('input_state', {})
|
||||
|
||||
|
||||
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
|
||||
if not self.seed:
|
||||
self.seed = struct.unpack('i', os.urandom(4))[0]
|
||||
|
||||
# Convert startouttext and endouttext to proper <text></text>
|
||||
problem_text = re.sub("startouttext\s*/", "text", problem_text)
|
||||
@@ -188,6 +189,7 @@ class LoncapaProblem(object):
|
||||
return {'seed': self.seed,
|
||||
'student_answers': self.student_answers,
|
||||
'correct_map': self.correct_map.get_dict(),
|
||||
'input_state': self.input_state,
|
||||
'done': self.done}
|
||||
|
||||
def get_max_score(self):
|
||||
@@ -237,6 +239,20 @@ class LoncapaProblem(object):
|
||||
self.correct_map.set_dict(cmap.get_dict())
|
||||
return cmap
|
||||
|
||||
def ungraded_response(self, xqueue_msg, queuekey):
|
||||
'''
|
||||
Handle any responses from the xqueue that do not contain grades
|
||||
Will try to pass the queue message to all inputtypes that can handle ungraded responses
|
||||
|
||||
Does not return any value
|
||||
'''
|
||||
# check against each inputtype
|
||||
for the_input in self.inputs.values():
|
||||
# if the input type has an ungraded function, pass in the values
|
||||
if hasattr(the_input, 'ungraded_response'):
|
||||
the_input.ungraded_response(xqueue_msg, queuekey)
|
||||
|
||||
|
||||
def is_queued(self):
|
||||
'''
|
||||
Returns True if any part of the problem has been submitted to an external queue
|
||||
@@ -351,7 +367,7 @@ class LoncapaProblem(object):
|
||||
dispatch = get['dispatch']
|
||||
return self.inputs[input_id].handle_ajax(dispatch, get)
|
||||
else:
|
||||
log.warning("Could not find matching input for id: %s" % problem_id)
|
||||
log.warning("Could not find matching input for id: %s" % input_id)
|
||||
return {}
|
||||
|
||||
|
||||
@@ -527,11 +543,15 @@ class LoncapaProblem(object):
|
||||
value = ""
|
||||
if self.student_answers and problemid in self.student_answers:
|
||||
value = self.student_answers[problemid]
|
||||
|
||||
|
||||
if input_id not in self.input_state:
|
||||
self.input_state[input_id] = {}
|
||||
|
||||
# do the rendering
|
||||
state = {'value': value,
|
||||
'status': status,
|
||||
'id': input_id,
|
||||
'input_state': self.input_state[input_id],
|
||||
'feedback': {'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode, }}
|
||||
|
||||
@@ -37,18 +37,18 @@ graded status as'status'
|
||||
# makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a
|
||||
# general css and layout strategy for capa, document it, then implement it.
|
||||
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
import re
|
||||
import shlex # for splitting quoted strings
|
||||
import sys
|
||||
import os
|
||||
import pyparsing
|
||||
|
||||
from .registry import TagRegistry
|
||||
from capa.chem import chemcalc
|
||||
import xqueue_interface
|
||||
from datetime import datetime
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -97,7 +97,8 @@ class Attribute(object):
|
||||
"""
|
||||
val = element.get(self.name)
|
||||
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:
|
||||
# not required, so return default
|
||||
@@ -132,6 +133,8 @@ class InputTypeBase(object):
|
||||
* 'id' -- the id of this input, typically
|
||||
"{problem-location}_{response-num}_{input-num}"
|
||||
* 'status' (answered, unanswered, unsubmitted)
|
||||
* 'input_state' -- dictionary containing any inputtype-specific state
|
||||
that has been preserved
|
||||
* 'feedback' (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt. Specifically 'message', 'hint',
|
||||
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
|
||||
@@ -149,7 +152,8 @@ class InputTypeBase(object):
|
||||
|
||||
self.id = state.get('id', xml.get('id'))
|
||||
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', '')
|
||||
|
||||
@@ -157,6 +161,7 @@ class InputTypeBase(object):
|
||||
self.msg = feedback.get('message', '')
|
||||
self.hint = feedback.get('hint', '')
|
||||
self.hintmode = feedback.get('hintmode', None)
|
||||
self.input_state = state.get('input_state', {})
|
||||
|
||||
# put hint above msg if it should be displayed
|
||||
if self.hintmode == 'always':
|
||||
@@ -169,14 +174,15 @@ class InputTypeBase(object):
|
||||
self.process_requirements()
|
||||
|
||||
# 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()
|
||||
except Exception as err:
|
||||
# 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]
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
@@ -186,7 +192,6 @@ class InputTypeBase(object):
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
def process_requirements(self):
|
||||
"""
|
||||
Subclasses can declare lists of required and optional attributes. This
|
||||
@@ -196,7 +201,8 @@ class InputTypeBase(object):
|
||||
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.
|
||||
"""
|
||||
# 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 = {}
|
||||
to_render = set()
|
||||
for a in self.get_attributes():
|
||||
@@ -226,7 +232,7 @@ class InputTypeBase(object):
|
||||
get: a dictionary containing the data that was sent with the ajax call
|
||||
|
||||
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
|
||||
|
||||
@@ -247,8 +253,9 @@ class InputTypeBase(object):
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'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())
|
||||
return context
|
||||
|
||||
@@ -371,7 +378,6 @@ class ChoiceGroup(InputTypeBase):
|
||||
return [Attribute("show_correctness", "always"),
|
||||
Attribute("submitted_message", "Answer received.")]
|
||||
|
||||
|
||||
def _extra_context(self):
|
||||
return {'input_type': self.html_input_type,
|
||||
'choices': self.choices,
|
||||
@@ -436,7 +442,6 @@ class JavascriptInput(InputTypeBase):
|
||||
Attribute('display_class', None),
|
||||
Attribute('display_file', None), ]
|
||||
|
||||
|
||||
def setup(self):
|
||||
# Need to provide a value that JSON can parse if there is no
|
||||
# student-supplied value yet.
|
||||
@@ -459,7 +464,6 @@ class TextLine(InputTypeBase):
|
||||
template = "textline.html"
|
||||
tags = ['textline']
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
@@ -474,12 +478,12 @@ class TextLine(InputTypeBase):
|
||||
|
||||
# Attributes below used in setup(), not rendered directly.
|
||||
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('preprocessorClassName', None, render=False),
|
||||
Attribute('preprocessorSrc', None, render=False),
|
||||
]
|
||||
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
self.do_math = bool(self.loaded_attributes['math'] or
|
||||
@@ -490,12 +494,12 @@ class TextLine(InputTypeBase):
|
||||
self.preprocessor = None
|
||||
if self.do_math:
|
||||
# Preprocessor to insert between raw input and Mathjax
|
||||
self.preprocessor = {'class_name': self.loaded_attributes['preprocessorClassName'],
|
||||
'script_src': self.loaded_attributes['preprocessorSrc']}
|
||||
self.preprocessor = {
|
||||
'class_name': self.loaded_attributes['preprocessorClassName'],
|
||||
'script_src': self.loaded_attributes['preprocessorSrc']}
|
||||
if None in self.preprocessor.values():
|
||||
self.preprocessor = None
|
||||
|
||||
|
||||
def _extra_context(self):
|
||||
return {'do_math': self.do_math,
|
||||
'preprocessor': self.preprocessor, }
|
||||
@@ -539,7 +543,8 @@ class FileSubmission(InputTypeBase):
|
||||
"""
|
||||
# Check if problem has been queued
|
||||
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':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
@@ -547,7 +552,6 @@ class FileSubmission(InputTypeBase):
|
||||
|
||||
def _extra_context(self):
|
||||
return {'queue_len': self.queue_len, }
|
||||
return context
|
||||
|
||||
registry.register(FileSubmission)
|
||||
|
||||
@@ -562,8 +566,9 @@ class CodeInput(InputTypeBase):
|
||||
|
||||
template = "codeinput.html"
|
||||
tags = ['codeinput',
|
||||
'textbox', # Another (older) name--at some point we may want to make it use a
|
||||
# non-codemirror editor.
|
||||
'textbox',
|
||||
# Another (older) name--at some point we may want to make it use a
|
||||
# non-codemirror editor.
|
||||
]
|
||||
|
||||
# pulled out for testing
|
||||
@@ -586,22 +591,29 @@ class CodeInput(InputTypeBase):
|
||||
Attribute('tabsize', 4, transform=int),
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
def setup_code_response_rendering(self):
|
||||
"""
|
||||
Implement special logic: handle queueing state, and default input.
|
||||
"""
|
||||
# if no student input yet, then use the default input given by the problem
|
||||
if not self.value:
|
||||
self.value = self.xml.text
|
||||
# if no student input yet, then use the default input given by the
|
||||
# problem
|
||||
if not self.value and self.xml.text:
|
||||
self.value = self.xml.text.strip()
|
||||
|
||||
# Check if problem has been queued
|
||||
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':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = self.submitted_msg
|
||||
|
||||
|
||||
def setup(self):
|
||||
''' setup this input type '''
|
||||
self.setup_code_response_rendering()
|
||||
|
||||
def _extra_context(self):
|
||||
"""Defined queue_len, add it """
|
||||
return {'queue_len': self.queue_len, }
|
||||
@@ -610,8 +622,164 @@ registry.register(CodeInput)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MatlabInput(CodeInput):
|
||||
'''
|
||||
InputType for handling Matlab code input
|
||||
|
||||
TODO: API_KEY will go away once we have a way to specify it per-course
|
||||
Example:
|
||||
<matlabinput rows="10" cols="80" tabsize="4">
|
||||
Initial Text
|
||||
<plot_payload>
|
||||
%api_key=API_KEY
|
||||
</plot_payload>
|
||||
</matlabinput>
|
||||
'''
|
||||
template = "matlabinput.html"
|
||||
tags = ['matlabinput']
|
||||
|
||||
plot_submitted_msg = ("Submitted. As soon as a response is returned, "
|
||||
"this message will be replaced by that feedback.")
|
||||
|
||||
def setup(self):
|
||||
'''
|
||||
Handle matlab-specific parsing
|
||||
'''
|
||||
self.setup_code_response_rendering()
|
||||
|
||||
xml = self.xml
|
||||
self.plot_payload = xml.findtext('./plot_payload')
|
||||
|
||||
# Check if problem has been queued
|
||||
self.queuename = 'matlab'
|
||||
self.queue_msg = ''
|
||||
if 'queue_msg' in self.input_state and self.status in ['queued','incomplete', 'unsubmitted']:
|
||||
self.queue_msg = self.input_state['queue_msg']
|
||||
if 'queued' in self.input_state and self.input_state['queuestate'] is not None:
|
||||
self.status = 'queued'
|
||||
self.queue_len = 1
|
||||
self.msg = self.plot_submitted_msg
|
||||
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
Handle AJAX calls directed to this input
|
||||
|
||||
Args:
|
||||
- dispatch (str) - indicates how we want this ajax call to be handled
|
||||
- get (dict) - dictionary of key-value pairs that contain useful data
|
||||
Returns:
|
||||
|
||||
'''
|
||||
|
||||
if dispatch == 'plot':
|
||||
return self._plot_data(get)
|
||||
return {}
|
||||
|
||||
def ungraded_response(self, queue_msg, queuekey):
|
||||
'''
|
||||
Handle the response from the XQueue
|
||||
Stores the response in the input_state so it can be rendered later
|
||||
|
||||
Args:
|
||||
- queue_msg (str) - message returned from the queue. The message to be rendered
|
||||
- queuekey (str) - a key passed to the queue. Will be matched up to verify that this is the response we're waiting for
|
||||
|
||||
Returns:
|
||||
nothing
|
||||
'''
|
||||
# check the queuekey against the saved queuekey
|
||||
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
|
||||
and self.input_state['queuekey'] == queuekey):
|
||||
msg = self._parse_data(queue_msg)
|
||||
# save the queue message so that it can be rendered later
|
||||
self.input_state['queue_msg'] = msg
|
||||
self.input_state['queuestate'] = None
|
||||
self.input_state['queuekey'] = None
|
||||
|
||||
def _extra_context(self):
|
||||
''' Set up additional context variables'''
|
||||
extra_context = {
|
||||
'queue_len': self.queue_len,
|
||||
'queue_msg': self.queue_msg
|
||||
}
|
||||
return extra_context
|
||||
|
||||
def _parse_data(self, queue_msg):
|
||||
'''
|
||||
Parses the message out of the queue message
|
||||
Args:
|
||||
queue_msg (str) - a JSON encoded string
|
||||
Returns:
|
||||
returns the value for the the key 'msg' in queue_msg
|
||||
'''
|
||||
try:
|
||||
result = json.loads(queue_msg)
|
||||
except (TypeError, ValueError):
|
||||
log.error("External message should be a JSON serialized dict."
|
||||
" Received queue_msg = %s" % queue_msg)
|
||||
raise
|
||||
msg = result['msg']
|
||||
return msg
|
||||
|
||||
|
||||
def _plot_data(self, get):
|
||||
'''
|
||||
AJAX handler for the plot button
|
||||
Args:
|
||||
get (dict) - should have key 'submission' which contains the student submission
|
||||
Returns:
|
||||
dict - 'success' - whether or not we successfully queued this submission
|
||||
- 'message' - message to be rendered in case of error
|
||||
'''
|
||||
# only send data if xqueue exists
|
||||
if self.system.xqueue is None:
|
||||
return {'success': False, 'message': 'Cannot connect to the queue'}
|
||||
|
||||
# pull relevant info out of get
|
||||
response = get['submission']
|
||||
|
||||
# construct xqueue headers
|
||||
qinterface = self.system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.utcnow(), xqueue_interface.dateformat)
|
||||
callback_url = self.system.xqueue['construct_callback']('ungraded_response')
|
||||
anonymous_student_id = self.system.anonymous_student_id
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
self.id)
|
||||
xheader = xqueue_interface.make_xheader(
|
||||
lms_callback_url = callback_url,
|
||||
lms_key = queuekey,
|
||||
queue_name = self.queuename)
|
||||
|
||||
# save the input state
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
|
||||
# construct xqueue body
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = {'grader_payload': self.plot_payload,
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': response}
|
||||
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body = json.dumps(contents))
|
||||
|
||||
return {'success': error == 0, 'message': msg}
|
||||
|
||||
|
||||
registry.register(MatlabInput)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class Schematic(InputTypeBase):
|
||||
"""
|
||||
InputType for the schematic editor
|
||||
"""
|
||||
|
||||
template = "schematicinput.html"
|
||||
@@ -630,7 +798,6 @@ class Schematic(InputTypeBase):
|
||||
Attribute('initial_value', None),
|
||||
Attribute('submit_analyses', None), ]
|
||||
|
||||
return context
|
||||
|
||||
registry.register(Schematic)
|
||||
|
||||
@@ -660,12 +827,12 @@ class ImageInput(InputTypeBase):
|
||||
Attribute('height'),
|
||||
Attribute('width'), ]
|
||||
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
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:
|
||||
# Note: we subtract 15 to compensate for the size of the dot on the screen.
|
||||
# (is a 30x30 image--lms/static/green-pointer.png).
|
||||
@@ -673,7 +840,6 @@ class ImageInput(InputTypeBase):
|
||||
else:
|
||||
(self.gx, self.gy) = (0, 0)
|
||||
|
||||
|
||||
def _extra_context(self):
|
||||
|
||||
return {'gx': self.gx,
|
||||
@@ -730,7 +896,7 @@ class VseprInput(InputTypeBase):
|
||||
|
||||
registry.register(VseprInput)
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ChemicalEquationInput(InputTypeBase):
|
||||
@@ -794,7 +960,8 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
result['error'] = "Couldn't parse formula: {0}".format(p)
|
||||
except Exception:
|
||||
# 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"
|
||||
|
||||
return result
|
||||
@@ -843,16 +1010,16 @@ class DragAndDropInput(InputTypeBase):
|
||||
'can_reuse': ""}
|
||||
|
||||
tag_attrs['target'] = {'id': Attribute._sentinel,
|
||||
'x': Attribute._sentinel,
|
||||
'y': Attribute._sentinel,
|
||||
'w': Attribute._sentinel,
|
||||
'h': Attribute._sentinel}
|
||||
'x': Attribute._sentinel,
|
||||
'y': Attribute._sentinel,
|
||||
'w': Attribute._sentinel,
|
||||
'h': Attribute._sentinel}
|
||||
|
||||
dic = dict()
|
||||
|
||||
for attr_name in tag_attrs[tag_type].keys():
|
||||
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:
|
||||
dic['label'] = dic['label'] or dic['id']
|
||||
@@ -865,7 +1032,7 @@ class DragAndDropInput(InputTypeBase):
|
||||
|
||||
# add labels to images?:
|
||||
self.no_labels = Attribute('no_labels',
|
||||
default="False").parse_from_xml(self.xml)
|
||||
default="False").parse_from_xml(self.xml)
|
||||
|
||||
to_js = dict()
|
||||
|
||||
@@ -874,16 +1041,16 @@ class DragAndDropInput(InputTypeBase):
|
||||
|
||||
# outline places on image where to drag adn drop
|
||||
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?
|
||||
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
|
||||
to_js['draggables'] = [parse(draggable, 'draggable') for draggable in
|
||||
self.xml.iterchildren('draggable')]
|
||||
self.xml.iterchildren('draggable')]
|
||||
# list of targets
|
||||
to_js['targets'] = [parse(target, 'target') for target in
|
||||
self.xml.iterchildren('target')]
|
||||
self.xml.iterchildren('target')]
|
||||
|
||||
# custom background color for labels:
|
||||
label_bg_color = Attribute('label_bg_color',
|
||||
@@ -896,7 +1063,7 @@ class DragAndDropInput(InputTypeBase):
|
||||
|
||||
registry.register(DragAndDropInput)
|
||||
|
||||
#--------------------------------------------------------------------------------------------------------------------
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EditAMoleculeInput(InputTypeBase):
|
||||
@@ -934,6 +1101,7 @@ registry.register(EditAMoleculeInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DesignProtein2dInput(InputTypeBase):
|
||||
"""
|
||||
An input type for design of a protein in 2D. Integrates with the Protex java applet.
|
||||
@@ -969,6 +1137,7 @@ registry.register(DesignProtein2dInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EditAGeneInput(InputTypeBase):
|
||||
"""
|
||||
An input type for editing a gene. Integrates with the genex java applet.
|
||||
@@ -1005,6 +1174,7 @@ registry.register(EditAGeneInput)
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
|
||||
class AnnotationInput(InputTypeBase):
|
||||
"""
|
||||
Input type for annotations: students can enter some notes or other text
|
||||
@@ -1037,13 +1207,14 @@ class AnnotationInput(InputTypeBase):
|
||||
def setup(self):
|
||||
xml = self.xml
|
||||
|
||||
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.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.title = xml.findtext('./title', 'Annotation Exercise')
|
||||
self.text = xml.findtext('./text')
|
||||
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.options = self._find_options()
|
||||
|
||||
@@ -1061,7 +1232,7 @@ class AnnotationInput(InputTypeBase):
|
||||
'id': index,
|
||||
'description': option.text,
|
||||
'choice': option.get('choice')
|
||||
} for (index, option) in enumerate(elements) ]
|
||||
} for (index, option) in enumerate(elements)]
|
||||
|
||||
def _validate_options(self):
|
||||
''' Raises a ValueError if the choice attribute is missing or invalid. '''
|
||||
@@ -1071,7 +1242,8 @@ class AnnotationInput(InputTypeBase):
|
||||
if choice is None:
|
||||
raise ValueError('Missing required choice attribute.')
|
||||
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):
|
||||
''' Unpacks the json input state into a dict. '''
|
||||
@@ -1089,20 +1261,20 @@ class AnnotationInput(InputTypeBase):
|
||||
|
||||
return {
|
||||
'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,
|
||||
}
|
||||
|
||||
def _extra_context(self):
|
||||
extra_context = {
|
||||
'title': self.title,
|
||||
'text': self.text,
|
||||
'comment': self.comment,
|
||||
'comment_prompt': self.comment_prompt,
|
||||
'tag_prompt': self.tag_prompt,
|
||||
'options': self.options,
|
||||
'return_to_annotation': self.return_to_annotation,
|
||||
'debug': self.debug
|
||||
'title': self.title,
|
||||
'text': self.text,
|
||||
'comment': self.comment,
|
||||
'comment_prompt': self.comment_prompt,
|
||||
'tag_prompt': self.tag_prompt,
|
||||
'options': self.options,
|
||||
'return_to_annotation': self.return_to_annotation,
|
||||
'debug': self.debug
|
||||
}
|
||||
|
||||
extra_context.update(self._unpack(self.value))
|
||||
@@ -1110,4 +1282,3 @@ class AnnotationInput(InputTypeBase):
|
||||
return extra_context
|
||||
|
||||
registry.register(AnnotationInput)
|
||||
|
||||
|
||||
@@ -128,21 +128,25 @@ class LoncapaResponse(object):
|
||||
|
||||
for abox in inputfields:
|
||||
if abox.tag not in self.allowed_inputfields:
|
||||
msg = "%s: cannot have input field %s" % (unicode(self), abox.tag)
|
||||
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
|
||||
msg = "%s: cannot have input field %s" % (
|
||||
unicode(self), abox.tag)
|
||||
msg += "\nSee XML source line %s" % getattr(
|
||||
xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
if self.max_inputfields and len(inputfields) > self.max_inputfields:
|
||||
msg = "%s: cannot have more than %s input fields" % (
|
||||
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)
|
||||
|
||||
for prop in self.required_attributes:
|
||||
if not xml.get(prop):
|
||||
msg = "Error in problem specification: %s missing required attribute %s" % (
|
||||
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)
|
||||
|
||||
# ordered list of answer_id values for this response
|
||||
@@ -163,7 +167,8 @@ class LoncapaResponse(object):
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_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'):
|
||||
self.setup_response()
|
||||
@@ -211,7 +216,8 @@ class LoncapaResponse(object):
|
||||
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
|
||||
'''
|
||||
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)
|
||||
return new_cmap
|
||||
|
||||
@@ -241,14 +247,17 @@ class LoncapaResponse(object):
|
||||
# callback procedure to a social hint generation system.
|
||||
if not hintfn in self.context:
|
||||
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)
|
||||
|
||||
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:
|
||||
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)
|
||||
return
|
||||
|
||||
@@ -270,17 +279,19 @@ class LoncapaResponse(object):
|
||||
|
||||
if (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)
|
||||
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)
|
||||
hintmode = hintgroup.get('mode', 'always')
|
||||
for hintpart in hintgroup.findall('hintpart'):
|
||||
if hintpart.get('on') in hints_to_show:
|
||||
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]
|
||||
new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
|
||||
log.debug('after hint: new_cmap = %s' % new_cmap)
|
||||
@@ -340,7 +351,6 @@ class LoncapaResponse(object):
|
||||
response_msg_div = etree.Element('div')
|
||||
response_msg_div.text = str(response_msg)
|
||||
|
||||
|
||||
# Set the css class of the message <div>
|
||||
response_msg_div.set("class", "response_message")
|
||||
|
||||
@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse):
|
||||
# until we decide on exactly how to solve this issue. For now, files are
|
||||
# manually being compiled to DATA_DIR/js/compiled.
|
||||
|
||||
#latestTimestamp = 0
|
||||
#basepath = self.system.filestore.root_path + '/js/'
|
||||
#for filename in (self.display_dependencies + [self.display]):
|
||||
# latestTimestamp = 0
|
||||
# basepath = self.system.filestore.root_path + '/js/'
|
||||
# for filename in (self.display_dependencies + [self.display]):
|
||||
# filepath = basepath + filename
|
||||
# timestamp = os.stat(filepath).st_mtime
|
||||
# if timestamp > latestTimestamp:
|
||||
# latestTimestamp = timestamp
|
||||
#
|
||||
#h = hashlib.md5()
|
||||
#h.update(self.answer_id + str(self.display_dependencies))
|
||||
#compiled_filename = 'compiled/' + h.hexdigest() + '.js'
|
||||
#compiled_filepath = basepath + compiled_filename
|
||||
# h = hashlib.md5()
|
||||
# h.update(self.answer_id + str(self.display_dependencies))
|
||||
# compiled_filename = 'compiled/' + h.hexdigest() + '.js'
|
||||
# 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')
|
||||
# for filename in (self.display_dependencies + [self.display]):
|
||||
# filepath = basepath + filename
|
||||
@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
id=self.xml.get('id'))[0]
|
||||
|
||||
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.grader_xml)
|
||||
@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse):
|
||||
self.display = self.display_xml.get("src")
|
||||
|
||||
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:
|
||||
self.generator_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:
|
||||
self.grader_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:
|
||||
self.display_dependencies = []
|
||||
|
||||
@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse):
|
||||
|
||||
return subprocess.check_output(subprocess_args, env=self.get_node_env())
|
||||
|
||||
|
||||
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,
|
||||
self.generator,
|
||||
json.dumps(self.generator_dependencies),
|
||||
@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse):
|
||||
params = {}
|
||||
|
||||
for param in self.xml.xpath('//*[@id=$id]//responseparam',
|
||||
id=self.xml.get('id')):
|
||||
id=self.xml.get('id')):
|
||||
|
||||
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
|
||||
|
||||
def prepare_inputfield(self):
|
||||
|
||||
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
|
||||
id=self.xml.get('id')):
|
||||
id=self.xml.get('id')):
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
|
||||
@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
escapedict)
|
||||
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)
|
||||
|
||||
def get_score(self, student_answers):
|
||||
@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse):
|
||||
if submission is None or submission == '':
|
||||
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,
|
||||
self.grader,
|
||||
json.dumps(self.grader_dependencies),
|
||||
@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse):
|
||||
json.dumps(self.params)]).split('\n')
|
||||
|
||||
all_correct = json.loads(outputs[0].strip())
|
||||
evaluation = outputs[1].strip()
|
||||
solution = outputs[2].strip()
|
||||
evaluation = outputs[1].strip()
|
||||
solution = outputs[2].strip()
|
||||
return (all_correct, evaluation, solution)
|
||||
|
||||
def get_answers(self):
|
||||
@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse):
|
||||
return {self.answer_id: self.solution}
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ChoiceResponse(LoncapaResponse):
|
||||
"""
|
||||
This response type is used when the student chooses from a discrete set of
|
||||
@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse):
|
||||
self.assign_choice_names()
|
||||
|
||||
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):
|
||||
'''
|
||||
@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
allowed_inputfields = ['choicegroup']
|
||||
|
||||
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()
|
||||
|
||||
# define correct choices (after calling secondary setup)
|
||||
@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
|
||||
# unicode(self), student_answers, self.correct_choices))
|
||||
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')
|
||||
else:
|
||||
return CorrectMap(self.answer_id, 'incorrect')
|
||||
@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse):
|
||||
return cmap
|
||||
|
||||
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))
|
||||
return amap
|
||||
|
||||
@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse):
|
||||
context = self.context
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
try:
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance_xml = xml.xpath(
|
||||
'//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance = contextualize_text(self.tolerance_xml, context)
|
||||
except Exception:
|
||||
self.tolerance = '0'
|
||||
@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse):
|
||||
try:
|
||||
correct_ans = complex(self.correct_answer)
|
||||
except ValueError:
|
||||
log.debug("Content error--answer '{0}' is not a valid complex number".format(self.correct_answer))
|
||||
raise StudentInputError("There was a problem with the staff answer to this problem")
|
||||
log.debug("Content error--answer '{0}' is not a valid complex number".format(
|
||||
self.correct_answer))
|
||||
raise StudentInputError(
|
||||
"There was a problem with the staff answer to this problem")
|
||||
|
||||
try:
|
||||
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer),
|
||||
correct_ans, self.tolerance)
|
||||
correct = compare_with_tolerance(
|
||||
evaluator(dict(), dict(), student_answer),
|
||||
correct_ans, self.tolerance)
|
||||
# We should catch this explicitly.
|
||||
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
|
||||
# But we'd need to confirm
|
||||
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
|
||||
type, value, traceback = sys.exc_info()
|
||||
|
||||
@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse):
|
||||
max_inputfields = 1
|
||||
|
||||
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):
|
||||
'''Grade a string response '''
|
||||
@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse):
|
||||
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
|
||||
|
||||
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
|
||||
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse):
|
||||
hints_to_show = []
|
||||
for hxml in hxml_set:
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(hxml.get('answer'), self.context).strip()
|
||||
if self.check_string(correct_answer, given): hints_to_show.append(name)
|
||||
correct_answer = contextualize_text(
|
||||
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)
|
||||
return hints_to_show
|
||||
|
||||
@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse):
|
||||
correct[0] ='incorrect'
|
||||
</answer>
|
||||
</customresponse>"""},
|
||||
{'snippet': """<script type="loncapa/python"><![CDATA[
|
||||
{'snippet': """<script type="loncapa/python"><![CDATA[
|
||||
|
||||
def sympy_check2():
|
||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
||||
@@ -907,15 +932,16 @@ def sympy_check2():
|
||||
response_tag = 'customresponse'
|
||||
|
||||
allowed_inputfields = ['textline', 'textbox', 'crystallography',
|
||||
'chemicalequationinput', 'vsepr_input',
|
||||
'drag_and_drop_input', 'editamoleculeinput',
|
||||
'designprotein2dinput', 'editageneinput',
|
||||
'annotationinput']
|
||||
'chemicalequationinput', 'vsepr_input',
|
||||
'drag_and_drop_input', 'editamoleculeinput',
|
||||
'designprotein2dinput', 'editageneinput',
|
||||
'annotationinput']
|
||||
|
||||
def setup_response(self):
|
||||
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.myid = xml.get('id')
|
||||
|
||||
@@ -939,7 +965,8 @@ def sympy_check2():
|
||||
if cfn in self.context:
|
||||
self.code = self.context[cfn]
|
||||
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',
|
||||
'<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
@@ -952,7 +979,8 @@ def sympy_check2():
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
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:
|
||||
self.code = answer.text
|
||||
|
||||
@@ -1032,7 +1060,7 @@ def sympy_check2():
|
||||
# any options to be passed to the cfn
|
||||
'options': self.xml.get('options'),
|
||||
'testdat': 'hello world',
|
||||
})
|
||||
})
|
||||
|
||||
# pass self.system.debug to cfn
|
||||
self.context['debug'] = self.system.DEBUG
|
||||
@@ -1049,7 +1077,8 @@ def sympy_check2():
|
||||
print "context = ", self.context
|
||||
print traceback.format_exc()
|
||||
# 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:
|
||||
# self.code is not a string; assume its a function
|
||||
|
||||
@@ -1058,18 +1087,22 @@ def sympy_check2():
|
||||
ret = None
|
||||
log.debug(" submission = %s" % submission)
|
||||
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
|
||||
# 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)
|
||||
nargs = len(argspec.args) - len(argspec.defaults or [])
|
||||
kwargs = {}
|
||||
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('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)
|
||||
except Exception as err:
|
||||
@@ -1077,7 +1110,8 @@ def sympy_check2():
|
||||
# print "context = ",self.context
|
||||
log.error(traceback.format_exc())
|
||||
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:
|
||||
|
||||
@@ -1086,7 +1120,8 @@ def sympy_check2():
|
||||
# If there are multiple inputs, they all get marked
|
||||
# to the same correct/incorrect value
|
||||
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 = self.clean_message_html(msg)
|
||||
|
||||
@@ -1097,7 +1132,6 @@ def sympy_check2():
|
||||
else:
|
||||
messages[0] = msg
|
||||
|
||||
|
||||
# Another kind of dictionary the check function can return has
|
||||
# the form:
|
||||
# {'overall_message': STRING,
|
||||
@@ -1113,21 +1147,25 @@ def sympy_check2():
|
||||
correct = []
|
||||
messages = []
|
||||
for input_dict in input_list:
|
||||
correct.append('correct' if input_dict['ok'] else 'incorrect')
|
||||
msg = self.clean_message_html(input_dict['msg']) if 'msg' in input_dict else None
|
||||
correct.append('correct'
|
||||
if input_dict['ok'] else 'incorrect')
|
||||
msg = (self.clean_message_html(input_dict['msg'])
|
||||
if 'msg' in input_dict else None)
|
||||
messages.append(msg)
|
||||
|
||||
# Otherwise, we do not recognize the dictionary
|
||||
# Raise an exception
|
||||
else:
|
||||
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,
|
||||
# indicating whether all inputs should be marked
|
||||
# correct or incorrect
|
||||
else:
|
||||
correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset)
|
||||
n = len(idset)
|
||||
correct = ['correct'] * n if ret else ['incorrect'] * n
|
||||
|
||||
# build map giving "correct"ness of the answer(s)
|
||||
correct_map = CorrectMap()
|
||||
@@ -1136,7 +1174,8 @@ def sympy_check2():
|
||||
correct_map.set_overall_message(overall_message)
|
||||
|
||||
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],
|
||||
npoints=npoints)
|
||||
return correct_map
|
||||
@@ -1232,8 +1271,9 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
|
||||
system.xqueue = { 'interface': XqueueInterface object,
|
||||
'callback_url': Per-StudentModule callback URL
|
||||
where results are posted (string),
|
||||
'construct_callback': Per-StudentModule callback URL
|
||||
constructor, defaults to using 'score_update'
|
||||
as the correct dispatch (function),
|
||||
'default_queuename': Default queuename to submit request (string)
|
||||
}
|
||||
|
||||
@@ -1242,7 +1282,7 @@ class CodeResponse(LoncapaResponse):
|
||||
"""
|
||||
|
||||
response_tag = 'coderesponse'
|
||||
allowed_inputfields = ['textbox', 'filesubmission']
|
||||
allowed_inputfields = ['textbox', 'filesubmission', 'matlabinput']
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
@@ -1263,7 +1303,8 @@ class CodeResponse(LoncapaResponse):
|
||||
self.queue_name = xml.get('queuename', default_queuename)
|
||||
|
||||
# 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')
|
||||
if codeparam is None:
|
||||
self._parse_externalresponse_xml()
|
||||
@@ -1277,12 +1318,14 @@ class CodeResponse(LoncapaResponse):
|
||||
self.answer (an answer to display to the student in the LMS)
|
||||
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 = grader_payload.text if grader_payload is not None else ''
|
||||
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',
|
||||
'No answer provided.')
|
||||
|
||||
@@ -1304,8 +1347,10 @@ class CodeResponse(LoncapaResponse):
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
code = self.context['script_code']
|
||||
if not code:
|
||||
msg = '%s: Missing answer script code for coderesponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
msg = '%s: Missing answer script code for coderesponse' % unicode(
|
||||
self)
|
||||
msg += "\nSee XML source line %s" % getattr(
|
||||
self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
tests = self.xml.get('tests')
|
||||
@@ -1320,7 +1365,8 @@ class CodeResponse(LoncapaResponse):
|
||||
try:
|
||||
exec(code, penv, penv)
|
||||
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)
|
||||
try:
|
||||
self.answer = penv['answer']
|
||||
@@ -1333,7 +1379,7 @@ class CodeResponse(LoncapaResponse):
|
||||
# Finally, make the ExternalResponse input XML format conform to the generic
|
||||
# exteral grader interface
|
||||
# The XML tagging of grader_payload is pyxserver-specific
|
||||
grader_payload = '<pyxserver>'
|
||||
grader_payload = '<pyxserver>'
|
||||
grader_payload += '<tests>' + tests + '</tests>\n'
|
||||
grader_payload += '<processor>' + code + '</processor>'
|
||||
grader_payload += '</pyxserver>'
|
||||
@@ -1346,14 +1392,14 @@ class CodeResponse(LoncapaResponse):
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: cannot get student answer for %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)
|
||||
|
||||
# We do not support xqueue within Studio.
|
||||
if self.system.xqueue is None:
|
||||
cmap = CorrectMap()
|
||||
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
|
||||
|
||||
# Prepare xqueue request
|
||||
@@ -1368,9 +1414,11 @@ class CodeResponse(LoncapaResponse):
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
self.answer_id)
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
callback_url = self.system.xqueue['construct_callback']()
|
||||
xheader = xqueue_interface.make_xheader(
|
||||
lms_callback_url=callback_url,
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
|
||||
# Generate body
|
||||
if is_list_of_files(submission):
|
||||
@@ -1381,13 +1429,16 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
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,
|
||||
'submission_time': qtime,
|
||||
}
|
||||
}
|
||||
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):
|
||||
# TODO: Is there any information we want to send here?
|
||||
contents.update({'student_response': ''})
|
||||
@@ -1415,13 +1466,15 @@ class CodeResponse(LoncapaResponse):
|
||||
# 2) Frontend: correctness='incomplete' eventually trickles down
|
||||
# through inputtypes.textbox and .filesubmission to inform the
|
||||
# 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
|
||||
|
||||
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:
|
||||
oldcmap.set(self.answer_id,
|
||||
msg='Invalid grader reply. Please contact the course staff.')
|
||||
@@ -1433,14 +1486,16 @@ class CodeResponse(LoncapaResponse):
|
||||
self.context['correct'] = correctness
|
||||
|
||||
# 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):
|
||||
# Sanity check on returned points
|
||||
if points < 0:
|
||||
points = 0
|
||||
# Queuestate is consumed
|
||||
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
|
||||
msg=msg.replace(' ', ' '), queuestate=None)
|
||||
oldcmap.set(
|
||||
self.answer_id, npoints=points, correctness=correctness,
|
||||
msg=msg.replace(' ', ' '), queuestate=None)
|
||||
else:
|
||||
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' %
|
||||
(queuekey, self.answer_id))
|
||||
@@ -1560,15 +1615,18 @@ main()
|
||||
if answer is not None:
|
||||
answer_src = answer.get('src')
|
||||
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:
|
||||
self.code = answer.text
|
||||
else:
|
||||
# no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
msg = '%s: Missing answer script code for externalresponse' % unicode(
|
||||
self)
|
||||
msg += "\nSee XML source line %s" % getattr(
|
||||
self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.tests = xml.get('tests')
|
||||
@@ -1591,10 +1649,12 @@ main()
|
||||
payload.update(extra_payload)
|
||||
|
||||
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)
|
||||
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)
|
||||
raise Exception(msg)
|
||||
|
||||
@@ -1602,13 +1662,15 @@ main()
|
||||
log.info('response = %s' % r.text)
|
||||
|
||||
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:
|
||||
# response is XML; parse it
|
||||
rxml = etree.fromstring(r.text)
|
||||
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)
|
||||
raise Exception(msg)
|
||||
|
||||
@@ -1633,7 +1695,8 @@ main()
|
||||
except Exception as err:
|
||||
log.error('Error %s' % err)
|
||||
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(
|
||||
self.answer_ids[0], 'msg',
|
||||
'<span class="inline-error">%s</span>' % str(err).replace('<', '<'))
|
||||
@@ -1650,7 +1713,8 @@ main()
|
||||
# create CorrectMap
|
||||
for key in idset:
|
||||
idx = idset.index(key)
|
||||
msg = rxml.find('message').text.replace(' ', ' ') if idx == 0 else None
|
||||
msg = rxml.find('message').text.replace(
|
||||
' ', ' ') if idx == 0 else None
|
||||
cmap.set(key, self.context['correct'][idx], msg=msg)
|
||||
|
||||
return cmap
|
||||
@@ -1665,7 +1729,8 @@ main()
|
||||
except Exception as err:
|
||||
log.error('Error %s' % err)
|
||||
if self.system.DEBUG:
|
||||
msg = '<span class="inline-error">%s</span>' % str(err).replace('<', '<')
|
||||
msg = '<span class="inline-error">%s</span>' % str(
|
||||
err).replace('<', '<')
|
||||
exans = [''] * len(self.answer_ids)
|
||||
exans[0] = msg
|
||||
|
||||
@@ -1712,8 +1777,9 @@ class FormulaResponse(LoncapaResponse):
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
self.samples = contextualize_text(xml.get('samples'), context)
|
||||
try:
|
||||
self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance_xml = xml.xpath(
|
||||
'//*[@id=$id]//responseparam[@type="tolerance"]/@default',
|
||||
id=xml.get('id'))[0]
|
||||
self.tolerance = contextualize_text(self.tolerance_xml, context)
|
||||
except Exception:
|
||||
self.tolerance = '0.00001'
|
||||
@@ -1735,14 +1801,15 @@ class FormulaResponse(LoncapaResponse):
|
||||
|
||||
def get_score(self, student_answers):
|
||||
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)
|
||||
|
||||
def check_formula(self, expected, given, samples):
|
||||
variables = samples.split('@')[0].split(',')
|
||||
numsamples = int(samples.split('@')[1].split('#')[1])
|
||||
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))
|
||||
for i in range(numsamples):
|
||||
@@ -1753,22 +1820,26 @@ class FormulaResponse(LoncapaResponse):
|
||||
value = random.uniform(*ranges[var])
|
||||
instructor_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(),
|
||||
expected, cs=self.case_sensitive)
|
||||
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,
|
||||
dict(),
|
||||
given,
|
||||
cs=self.case_sensitive)
|
||||
except UndefinedVariable as uv:
|
||||
log.debug('formularesponse: undefined variable in given=%s' % given)
|
||||
raise StudentInputError("Invalid input: " + uv.message + " not permitted in answer")
|
||||
log.debug(
|
||||
'formularesponse: undefined variable in given=%s' % given)
|
||||
raise StudentInputError(
|
||||
"Invalid input: " + uv.message + " not permitted in answer")
|
||||
except Exception as err:
|
||||
#traceback.print_exc()
|
||||
# traceback.print_exc()
|
||||
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))
|
||||
if numpy.isnan(student_result) or numpy.isinf(student_result):
|
||||
return "incorrect"
|
||||
@@ -1792,9 +1863,11 @@ class FormulaResponse(LoncapaResponse):
|
||||
for hxml in hxml_set:
|
||||
samples = hxml.get('samples')
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(hxml.get('answer'), self.context)
|
||||
correct_answer = contextualize_text(
|
||||
hxml.get('answer'), self.context)
|
||||
try:
|
||||
correctness = self.check_formula(correct_answer, given, samples)
|
||||
correctness = self.check_formula(
|
||||
correct_answer, given, samples)
|
||||
except Exception:
|
||||
correctness = 'incorrect'
|
||||
if correctness == 'correct':
|
||||
@@ -1825,11 +1898,13 @@ class SchematicResponse(LoncapaResponse):
|
||||
|
||||
def get_score(self, student_answers):
|
||||
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})
|
||||
exec self.code in global_context, self.context
|
||||
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
|
||||
|
||||
def get_answers(self):
|
||||
@@ -1891,12 +1966,14 @@ class ImageResponse(LoncapaResponse):
|
||||
expectedset = self.get_answers()
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput>
|
||||
# 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')
|
||||
if not given: # No answer to parse. Mark as incorrect and move on
|
||||
continue
|
||||
# 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:
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] '
|
||||
'error grading %s (input=%s)' % (aid, given))
|
||||
@@ -1904,20 +1981,24 @@ class ImageResponse(LoncapaResponse):
|
||||
|
||||
rectangles, regions = expectedset
|
||||
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(';')
|
||||
for solution_rectangle in solution_rectangles:
|
||||
# parse expected answer
|
||||
# TODO: Compile regexp on file load
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
|
||||
solution_rectangle.strip().replace(' ', ''))
|
||||
m = re.match(
|
||||
'[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
|
||||
solution_rectangle.strip().replace(' ', ''))
|
||||
if not m:
|
||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
|
||||
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()]
|
||||
|
||||
# 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):
|
||||
correct_map.set(aid, 'correct')
|
||||
break
|
||||
@@ -1938,10 +2019,13 @@ class ImageResponse(LoncapaResponse):
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]),
|
||||
dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements]))
|
||||
return (
|
||||
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):
|
||||
'''
|
||||
Checking of annotation responses.
|
||||
@@ -1952,7 +2036,8 @@ class AnnotationResponse(LoncapaResponse):
|
||||
response_tag = 'annotationresponse'
|
||||
allowed_inputfields = ['annotationinput']
|
||||
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):
|
||||
xml = self.xml
|
||||
self.scoring_map = self._get_scoring_map()
|
||||
@@ -1966,7 +2051,8 @@ class AnnotationResponse(LoncapaResponse):
|
||||
student_option = self._get_submitted_option_id(student_answer)
|
||||
|
||||
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)
|
||||
if is_valid:
|
||||
@@ -1981,7 +2067,7 @@ class AnnotationResponse(LoncapaResponse):
|
||||
def _get_scoring_map(self):
|
||||
''' Returns a dict of option->scoring for each input. '''
|
||||
scoring = self.default_scoring
|
||||
choices = dict([(choice,choice) for choice in scoring])
|
||||
choices = dict([(choice, choice) for choice in scoring])
|
||||
scoring_map = {}
|
||||
|
||||
for inputfield in self.inputfields:
|
||||
@@ -1998,9 +2084,11 @@ class AnnotationResponse(LoncapaResponse):
|
||||
''' Returns a dict of answers for each input.'''
|
||||
answer_map = {}
|
||||
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:
|
||||
answer_map[inputfield.get('id')] = correct_option.get('description')
|
||||
answer_map[inputfield.get(
|
||||
'id')] = correct_option.get('description')
|
||||
return answer_map
|
||||
|
||||
def _get_max_points(self):
|
||||
@@ -2016,7 +2104,7 @@ class AnnotationResponse(LoncapaResponse):
|
||||
'id': index,
|
||||
'description': option.text,
|
||||
'choice': option.get('choice')
|
||||
} for (index, option) in enumerate(elements) ]
|
||||
} for (index, option) in enumerate(elements)]
|
||||
|
||||
def _find_option_with_choice(self, inputfield, choice):
|
||||
''' Returns the option with the given choice value, otherwise None. '''
|
||||
|
||||
117
common/lib/capa/capa/templates/matlabinput.html
Normal file
117
common/lib/capa/capa/templates/matlabinput.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<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="external-grader-message">
|
||||
${queue_msg|n}
|
||||
</div>
|
||||
|
||||
<div class="plot-button">
|
||||
<input type="button" class="save" name="plot-button" id="plot_${id}" 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: "matlab",
|
||||
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))});
|
||||
|
||||
var gentle_alert = function (parent_elt, msg) {
|
||||
if($(parent_elt).find('.capa_alert').length) {
|
||||
$(parent_elt).find('.capa_alert').remove();
|
||||
}
|
||||
var alert_elem = "<div>" + msg + "</div>";
|
||||
alert_elem = $(alert_elem).addClass('capa_alert');
|
||||
$(parent_elt).find('.action').after(alert_elem);
|
||||
$(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700);
|
||||
}
|
||||
|
||||
|
||||
// hook up the plot button
|
||||
var plot = function(event) {
|
||||
var problem_elt = $(event.target).closest('.problems-wrapper');
|
||||
url = $(event.target).closest('.problems-wrapper').data('url');
|
||||
input_id = "${id}";
|
||||
|
||||
// save the codemirror text to the textarea
|
||||
cm.save();
|
||||
var input = $("#input_${id}");
|
||||
// pull out the coded text
|
||||
submission = input.val();
|
||||
|
||||
answer = input.serialize();
|
||||
|
||||
// setup callback for after we send information to plot
|
||||
var plot_callback = function(response) {
|
||||
if(response.success) {
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
gentle_alert(problem_elt, msg);
|
||||
}
|
||||
}
|
||||
|
||||
var save_callback = function(response) {
|
||||
if(response.success) {
|
||||
// send information to the problem's plot functionality
|
||||
Problem.inputAjax(url, input_id, 'plot',
|
||||
{'submission': submission}, plot_callback);
|
||||
}
|
||||
else {
|
||||
gentle_alert(problem_elt, msg);
|
||||
}
|
||||
}
|
||||
|
||||
// save the answer
|
||||
$.postWithPrefix(url + '/problem_save', answer, save_callback);
|
||||
|
||||
}
|
||||
$('#plot_${id}').click(plot);
|
||||
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
@@ -2,7 +2,7 @@ import fs
|
||||
import fs.osfs
|
||||
import os
|
||||
|
||||
from mock import Mock
|
||||
from mock import Mock, MagicMock
|
||||
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
@@ -16,6 +16,11 @@ def tst_render_template(template, context):
|
||||
"""
|
||||
return '<div>{0}</div>'.format(saxutils.escape(repr(context)))
|
||||
|
||||
def calledback_url(dispatch = 'score_update'):
|
||||
return dispatch
|
||||
|
||||
xqueue_interface = MagicMock()
|
||||
xqueue_interface.send_to_queue.return_value = (0, 'Success!')
|
||||
|
||||
test_system = Mock(
|
||||
ajax_url='courses/course_id/modx/a_location',
|
||||
@@ -26,7 +31,7 @@ test_system = Mock(
|
||||
user=Mock(),
|
||||
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
|
||||
debug=True,
|
||||
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
|
||||
anonymous_student_id='student'
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import xml.sax.saxutils as saxutils
|
||||
|
||||
from . import test_system
|
||||
from capa import inputtypes
|
||||
from mock import ANY
|
||||
|
||||
# just a handy shortcut
|
||||
lookup_tag = inputtypes.registry.get_class_for_tag
|
||||
@@ -300,6 +301,98 @@ class CodeInputTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
class MatlabTest(unittest.TestCase):
|
||||
'''
|
||||
Test Matlab input types
|
||||
'''
|
||||
def setUp(self):
|
||||
self.rows = '10'
|
||||
self.cols = '80'
|
||||
self.tabsize = '4'
|
||||
self.mode = ""
|
||||
self.payload = "payload"
|
||||
self.linenumbers = 'true'
|
||||
self.xml = """<matlabinput id="prob_1_2"
|
||||
rows="{r}" cols="{c}"
|
||||
tabsize="{tabsize}" mode="{m}"
|
||||
linenumbers="{ln}">
|
||||
<plot_payload>
|
||||
{payload}
|
||||
</plot_payload>
|
||||
</matlabinput>""".format(r = self.rows,
|
||||
c = self.cols,
|
||||
tabsize = self.tabsize,
|
||||
m = self.mode,
|
||||
payload = self.payload,
|
||||
ln = self.linenumbers)
|
||||
elt = etree.fromstring(self.xml)
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'feedback': {'message': '3'}, }
|
||||
|
||||
self.input_class = lookup_tag('matlabinput')
|
||||
self.the_input = self.input_class(test_system, elt, state)
|
||||
|
||||
|
||||
def test_rendering(self):
|
||||
context = self.the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_rendering_with_state(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'input_state': {'queue_msg': 'message'},
|
||||
'feedback': {'message': '3'}, }
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
input_class = lookup_tag('matlabinput')
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': 'message',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_plot_data(self):
|
||||
get = {'submission': 'x = 1234;'}
|
||||
response = self.the_input.handle_ajax("plot", get)
|
||||
|
||||
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
|
||||
|
||||
self.assertTrue(response['success'])
|
||||
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
|
||||
self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
|
||||
|
||||
|
||||
|
||||
|
||||
class SchematicTest(unittest.TestCase):
|
||||
'''
|
||||
|
||||
@@ -93,6 +93,7 @@ class CapaFields(object):
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={})
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
|
||||
'done': self.done,
|
||||
'correct_map': self.correct_map,
|
||||
'student_answers': self.student_answers,
|
||||
'input_state': self.input_state,
|
||||
'seed': self.seed,
|
||||
}
|
||||
|
||||
@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
|
||||
lcp_state = self.lcp.get_state()
|
||||
self.done = lcp_state['done']
|
||||
self.correct_map = lcp_state['correct_map']
|
||||
self.input_state = lcp_state['input_state']
|
||||
self.student_answers = lcp_state['student_answers']
|
||||
self.seed = lcp_state['seed']
|
||||
|
||||
@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
|
||||
'problem_save': self.save_problem,
|
||||
'problem_show': self.get_answer,
|
||||
'score_update': self.update_score,
|
||||
'input_ajax': self.lcp.handle_input_ajax
|
||||
'input_ajax': self.handle_input_ajax,
|
||||
'ungraded_response': self.handle_ungraded_response
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -537,6 +541,43 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def handle_ungraded_response(self, get):
|
||||
'''
|
||||
Delivers a response from the XQueue to the capa problem
|
||||
|
||||
The score of the problem will not be updated
|
||||
|
||||
Args:
|
||||
- get (dict) must contain keys:
|
||||
queuekey - a key specific to this response
|
||||
xqueue_body - the body of the response
|
||||
Returns:
|
||||
empty dictionary
|
||||
|
||||
No ajax return is needed, so an empty dict is returned
|
||||
'''
|
||||
queuekey = get['queuekey']
|
||||
score_msg = get['xqueue_body']
|
||||
# pass along the xqueue message to the problem
|
||||
self.lcp.ungraded_response(score_msg, queuekey)
|
||||
self.set_state_from_lcp()
|
||||
return dict()
|
||||
|
||||
def handle_input_ajax(self, get):
|
||||
'''
|
||||
Handle ajax calls meant for a particular input in the problem
|
||||
|
||||
Args:
|
||||
- get (dict) - data that should be passed to the input
|
||||
Returns:
|
||||
- dict containing the response from the input
|
||||
'''
|
||||
response = self.lcp.handle_input_ajax(get)
|
||||
# save any state changes that may occur
|
||||
self.set_state_from_lcp()
|
||||
return response
|
||||
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
For the "show answer" button.
|
||||
|
||||
@@ -41,6 +41,11 @@ class @Problem
|
||||
@el.attr progress: response.progress_status
|
||||
@el.trigger('progressChanged')
|
||||
|
||||
forceUpdate: (response) =>
|
||||
@el.attr progress: response.progress_status
|
||||
@el.trigger('progressChanged')
|
||||
|
||||
|
||||
queueing: =>
|
||||
@queued_items = @$(".xqueue")
|
||||
@num_queued_items = @queued_items.length
|
||||
@@ -71,6 +76,7 @@ class @Problem
|
||||
|
||||
@num_queued_items = @new_queued_items.length
|
||||
if @num_queued_items == 0
|
||||
@forceUpdate response
|
||||
delete window.queuePollerID
|
||||
else
|
||||
# TODO: Some logic to dynamically adjust polling rate based on queuelen
|
||||
|
||||
@@ -174,7 +174,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
str(len(self.child_history)))
|
||||
|
||||
xheader = xqueue_interface.make_xheader(
|
||||
lms_callback_url=system.xqueue['callback_url'],
|
||||
lms_callback_url=system.xqueue['construct_callback'](),
|
||||
lms_key=queuekey,
|
||||
queue_name=self.message_queue_name
|
||||
)
|
||||
@@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
anonymous_student_id +
|
||||
str(len(self.child_history)))
|
||||
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'],
|
||||
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](),
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queue_name)
|
||||
|
||||
|
||||
@@ -183,7 +183,10 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
self.test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
|
||||
self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue',
|
||||
def constructed_callback(dispatch="score_update"):
|
||||
return dispatch
|
||||
|
||||
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue',
|
||||
'waittime': 1}
|
||||
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
@@ -181,12 +181,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
host=request.get_host(),
|
||||
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
|
||||
)
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch='score_update'),
|
||||
)
|
||||
|
||||
def make_xqueue_callback(dispatch='score_update'):
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = '{proto}://{host}'.format(
|
||||
host=request.get_host(),
|
||||
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
|
||||
)
|
||||
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch=dispatch),
|
||||
)
|
||||
return xqueue_callback_url
|
||||
|
||||
# Default queuename is course-specific and is derived from the course that
|
||||
# contains the current module.
|
||||
@@ -194,7 +203,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
|
||||
|
||||
xqueue = {'interface': xqueue_interface,
|
||||
'callback_url': xqueue_callback_url,
|
||||
'construct_callback': make_xqueue_callback,
|
||||
'default_queuename': xqueue_default_queuename.replace(' ', '_'),
|
||||
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user