Commit d711d29d by Sarina Canelake

Clean up pep8/pylint

parent e071ebb9
...@@ -35,17 +35,17 @@ from calc import evaluator, UndefinedVariable ...@@ -35,17 +35,17 @@ from calc import evaluator, UndefinedVariable
from . import correctmap from . import correctmap
from datetime import datetime from datetime import datetime
from pytz import UTC from pytz import UTC
from .util import * from .util import compare_with_tolerance, contextualize_text, convert_files_to_filenames, is_list_of_files, find_with_default
from lxml import etree from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
import safe_exec import capa.safe_exec as safe_exec
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
CorrectMap = correctmap.CorrectMap CorrectMap = correctmap.CorrectMap # pylint: disable=C0103
CORRECTMAP_PY = None CORRECTMAP_PY = None
...@@ -1181,7 +1181,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1181,7 +1181,7 @@ class CustomResponse(LoncapaResponse):
fn = self.code fn = self.code
answer_given = submission[0] if (len(idset) == 1) else submission answer_given = submission[0] if (len(idset) == 1) else submission
kwnames = self.xml.get("cfn_extra_args", "").split() kwnames = self.xml.get("cfn_extra_args", "").split()
kwargs = {n:self.context.get(n) for n in kwnames} kwargs = {n: self.context.get(n) for n in kwnames}
log.debug(" submission = %s" % submission) log.debug(" submission = %s" % submission)
try: try:
ret = fn(self.expect, answer_given, **kwargs) ret = fn(self.expect, answer_given, **kwargs)
...@@ -1304,7 +1304,7 @@ class CustomResponse(LoncapaResponse): ...@@ -1304,7 +1304,7 @@ class CustomResponse(LoncapaResponse):
# Notify student with a student input error # Notify student with a student input error
_, _, traceback_obj = sys.exc_info() _, _, traceback_obj = sys.exc_info()
raise ResponseError, err.message, traceback_obj raise ResponseError(err.message, traceback_obj)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1597,15 +1597,25 @@ class CodeResponse(LoncapaResponse): ...@@ -1597,15 +1597,25 @@ class CodeResponse(LoncapaResponse):
class ExternalResponse(LoncapaResponse): class ExternalResponse(LoncapaResponse):
''' """
Grade the students input using an external server. Grade the students input using an external server.
Typically used by coding problems. Typically used by coding problems.
''' """
response_tag = 'externalresponse' response_tag = 'externalresponse'
allowed_inputfields = ['textline', 'textbox'] allowed_inputfields = ['textline', 'textbox']
awdmap = {
'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
'WRONG_FORMAT': 'incorrect',
}
def __init__(self, *args, **kwargs):
self.url = ''
self.tests = []
self.code = ''
super(ExternalResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
...@@ -1633,16 +1643,17 @@ class ExternalResponse(LoncapaResponse): ...@@ -1633,16 +1643,17 @@ class ExternalResponse(LoncapaResponse):
self.tests = xml.get('tests') self.tests = xml.get('tests')
def do_external_request(self, cmd, extra_payload): def do_external_request(self, cmd, extra_payload):
''' """
Perform HTTP request / post to external server. Perform HTTP request / post to external server.
cmd = remote command to perform (str) cmd = remote command to perform (str)
extra_payload = dict of extra stuff to post. extra_payload = dict of extra stuff to post.
Return XML tree of response (from response body) Return XML tree of response (from response body)
''' """
xmlstr = etree.tostring(self.xml, pretty_print=True) xmlstr = etree.tostring(self.xml, pretty_print=True)
payload = {'xml': xmlstr, payload = {
'xml': xmlstr,
'edX_cmd': cmd, 'edX_cmd': cmd,
'edX_tests': self.tests, 'edX_tests': self.tests,
'processor': self.code, 'processor': self.code,
...@@ -1652,26 +1663,24 @@ class ExternalResponse(LoncapaResponse): ...@@ -1652,26 +1663,24 @@ class ExternalResponse(LoncapaResponse):
try: try:
# call external server. TODO: synchronous call, can block for a # call external server. TODO: synchronous call, can block for a
# long time # long time
r = requests.post(self.url, data=payload) req = requests.post(self.url, data=payload)
except Exception as err: except Exception as err:
msg = 'Error %s - cannot connect to external server url=%s' % ( msg = 'Error {0} - cannot connect to external server url={1}'.format(err, self.url)
err, self.url)
log.error(msg) log.error(msg)
raise Exception(msg) raise Exception(msg)
if self.system.DEBUG: if self.system.DEBUG:
log.info('response = %s' % r.text) log.info('response = %s', req.text)
if (not r.text) or (not r.text.strip()): if (not req.text) or (not req.text.strip()):
raise Exception( raise Exception(
'Error: no response from external server url=%s' % self.url) '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(req.text)
except Exception as err: except Exception as err:
msg = 'Error %s - cannot parse response from external server r.text=%s' % ( msg = 'Error {0} - cannot parse response from external server req.text={1}'.format(err, req.text)
err, r.text)
log.error(msg) log.error(msg)
raise Exception(msg) raise Exception(msg)
...@@ -1682,9 +1691,13 @@ class ExternalResponse(LoncapaResponse): ...@@ -1682,9 +1691,13 @@ class ExternalResponse(LoncapaResponse):
cmap = CorrectMap() cmap = CorrectMap()
try: try:
submission = [student_answers[k] for k in idset] submission = [student_answers[k] for k in idset]
except Exception as err: except Exception as err: # pylint: disable=W0703
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % log.error(
(err, self.answer_ids, student_answers)) 'Error %s: cannot get student answer for %s; student_answers=%s',
err,
self.answer_ids,
student_answers
)
raise Exception(err) raise Exception(err)
self.context.update({'submission': submission}) self.context.update({'submission': submission})
...@@ -1693,8 +1706,8 @@ class ExternalResponse(LoncapaResponse): ...@@ -1693,8 +1706,8 @@ class ExternalResponse(LoncapaResponse):
try: try:
rxml = self.do_external_request('get_score', extra_payload) rxml = self.do_external_request('get_score', extra_payload)
except Exception as err: except Exception as err: # pylint: disable=W0703
log.error('Error %s' % err) log.error('Error %s', err)
if self.system.DEBUG: if self.system.DEBUG:
cmap.set_dict(dict(zip(sorted( cmap.set_dict(dict(zip(sorted(
self.answer_ids), ['incorrect'] * len(idset)))) self.answer_ids), ['incorrect'] * len(idset))))
...@@ -1703,13 +1716,11 @@ class ExternalResponse(LoncapaResponse): ...@@ -1703,13 +1716,11 @@ class ExternalResponse(LoncapaResponse):
'<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;')) '<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;'))
return cmap return cmap
ad = rxml.find('awarddetail').text awd = rxml.find('awarddetail').text
admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
'WRONG_FORMAT': 'incorrect',
}
self.context['correct'] = ['correct'] self.context['correct'] = ['correct']
if ad in admap: if awd in self.awdmap:
self.context['correct'][0] = admap[ad] self.context['correct'][0] = self.awdmap[awd]
# create CorrectMap # create CorrectMap
for key in idset: for key in idset:
...@@ -1721,14 +1732,14 @@ class ExternalResponse(LoncapaResponse): ...@@ -1721,14 +1732,14 @@ class ExternalResponse(LoncapaResponse):
return cmap return cmap
def get_answers(self): def get_answers(self):
''' """
Use external server to get expected answers Use external server to get expected answers
''' """
try: try:
rxml = self.do_external_request('get_answers', {}) rxml = self.do_external_request('get_answers', {})
exans = json.loads(rxml.find('expected').text) exans = json.loads(rxml.find('expected').text)
except Exception as err: except Exception as err: # pylint: disable=W0703
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( msg = '<span class="inline-error">%s</span>' % str(
err).replace('<', '&lt;') err).replace('<', '&lt;')
...@@ -1736,8 +1747,8 @@ class ExternalResponse(LoncapaResponse): ...@@ -1736,8 +1747,8 @@ class ExternalResponse(LoncapaResponse):
exans[0] = msg exans[0] = msg
if not (len(exans) == len(self.answer_ids)): if not (len(exans) == len(self.answer_ids)):
log.error('Expected %d answers from external server, only got %d!' % log.error('Expected %s answers from external server, only got %s!',
(len(self.answer_ids), len(exans))) len(self.answer_ids), len(exans))
raise Exception('Short response from external server') raise Exception('Short response from external server')
return dict(zip(self.answer_ids, exans)) return dict(zip(self.answer_ids, exans))
...@@ -1745,9 +1756,9 @@ class ExternalResponse(LoncapaResponse): ...@@ -1745,9 +1756,9 @@ class ExternalResponse(LoncapaResponse):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class FormulaResponse(LoncapaResponse): class FormulaResponse(LoncapaResponse):
''' """
Checking of symbolic math response using numerical sampling. Checking of symbolic math response using numerical sampling.
''' """
response_tag = 'formularesponse' response_tag = 'formularesponse'
hint_tag = 'formulahint' hint_tag = 'formulahint'
...@@ -1776,11 +1787,11 @@ class FormulaResponse(LoncapaResponse): ...@@ -1776,11 +1787,11 @@ class FormulaResponse(LoncapaResponse):
if tolerance_xml: # If it isn't an empty list... if tolerance_xml: # If it isn't an empty list...
self.tolerance = contextualize_text(tolerance_xml[0], context) self.tolerance = contextualize_text(tolerance_xml[0], context)
ts = xml.get('type') types = xml.get('type')
if ts is None: if types is None:
typeslist = [] typeslist = []
else: else:
typeslist = ts.split(',') typeslist = types.split(',')
if 'ci' in typeslist: if 'ci' in typeslist:
# Case insensitive # Case insensitive
self.case_sensitive = False self.case_sensitive = False
...@@ -1812,30 +1823,33 @@ class FormulaResponse(LoncapaResponse): ...@@ -1812,30 +1823,33 @@ class FormulaResponse(LoncapaResponse):
answer, answer,
case_sensitive=self.case_sensitive, case_sensitive=self.case_sensitive,
)) ))
except UndefinedVariable as uv: except UndefinedVariable as err:
log.debug( log.debug(
'formularesponse: undefined variable in formula=%s' % answer) 'formularesponse: undefined variable in formula=%s',
cgi.escape(answer)
)
raise StudentInputError( raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer" "Invalid input: " + err.message + " not permitted in answer"
) )
except ValueError as ve: except ValueError as err:
if 'factorial' in ve.message: if 'factorial' in err.message:
# This is thrown when fact() or factorial() is used in a formularesponse answer # This is thrown when fact() or factorial() is used in a formularesponse answer
# that tests on negative and/or non-integer inputs # that tests on negative and/or non-integer inputs
# ve.message will be: `factorial() only accepts integral values` or # err.message will be: `factorial() only accepts integral values` or
# `factorial() not defined for negative values` # `factorial() not defined for negative values`
log.debug( log.debug(
('formularesponse: factorial function used in response ' ('formularesponse: factorial function used in response '
'that tests negative and/or non-integer inputs. ' 'that tests negative and/or non-integer inputs. '
'given={0}').format(given) 'Provided answer was: %s'),
cgi.escape(answer)
) )
raise StudentInputError( raise StudentInputError(
("factorial function not permitted in answer " ("factorial function not permitted in answer "
"for this problem. Provided answer was: " "for this problem. Provided answer was: "
"{0}").format(cgi.escape(given)) "{0}").format(cgi.escape(answer))
) )
# If non-factorial related ValueError thrown, handle it the same as any other Exception # If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve)) 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(answer)) cgi.escape(answer))
except Exception as err: except Exception as err:
...@@ -1857,7 +1871,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1857,7 +1871,7 @@ class FormulaResponse(LoncapaResponse):
ranges = dict(zip(variables, sranges)) ranges = dict(zip(variables, sranges))
out = [] out = []
for i in range(numsamples): for _ in range(numsamples):
var_dict = {} var_dict = {}
# ranges give numerical ranges for testing # ranges give numerical ranges for testing
for var in ranges: for var in ranges:
...@@ -1902,15 +1916,17 @@ class FormulaResponse(LoncapaResponse): ...@@ -1902,15 +1916,17 @@ class FormulaResponse(LoncapaResponse):
except StudentInputError: except StudentInputError:
return False return False
def strip_dict(self, d): def strip_dict(self, inp_d):
''' Takes a dict. Returns an identical dict, with all non-word """
Takes a dict. Returns an identical dict, with all non-word
keys and all non-numeric values stripped out. All values also keys and all non-numeric values stripped out. All values also
converted to float. Used so we can safely use Python contexts. converted to float. Used so we can safely use Python contexts.
''' """
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and inp_d = dict([(k, numpy.complex(inp_d[k]))
for k in inp_d if type(k) == str and
k.isalnum() and k.isalnum() and
isinstance(d[k], numbers.Number)]) isinstance(inp_d[k], numbers.Number)])
return d return inp_d
def check_hint_condition(self, hxml_set, student_answers): def check_hint_condition(self, hxml_set, student_answers):
given = student_answers[self.answer_id] given = student_answers[self.answer_id]
...@@ -1920,14 +1936,18 @@ class FormulaResponse(LoncapaResponse): ...@@ -1920,14 +1936,18 @@ class FormulaResponse(LoncapaResponse):
name = hxml.get('name') name = hxml.get('name')
correct_answer = contextualize_text( correct_answer = contextualize_text(
hxml.get('answer'), self.context) hxml.get('answer'), self.context)
# pylint: disable=W0703
try: try:
correctness = self.check_formula( correctness = self.check_formula(
correct_answer, given, samples) correct_answer,
given,
samples
)
except Exception: except Exception:
correctness = 'incorrect' correctness = 'incorrect'
if correctness == 'correct': if correctness == 'correct':
hints_to_show.append(name) 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
def get_answers(self): def get_answers(self):
...@@ -1937,10 +1957,16 @@ class FormulaResponse(LoncapaResponse): ...@@ -1937,10 +1957,16 @@ class FormulaResponse(LoncapaResponse):
class SchematicResponse(LoncapaResponse): class SchematicResponse(LoncapaResponse):
"""
Circuit schematic response type.
"""
response_tag = 'schematicresponse' response_tag = 'schematicresponse'
allowed_inputfields = ['schematic'] allowed_inputfields = ['schematic']
def __init__(self, *args, **kwargs):
self.code = ''
super(SchematicResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
...@@ -2010,6 +2036,10 @@ class ImageResponse(LoncapaResponse): ...@@ -2010,6 +2036,10 @@ class ImageResponse(LoncapaResponse):
response_tag = 'imageresponse' response_tag = 'imageresponse'
allowed_inputfields = ['imageinput'] allowed_inputfields = ['imageinput']
def __init__(self, *args, **kwargs):
self.ielements = []
super(ImageResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
self.ielements = self.inputfields self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements] self.answer_ids = [ie.get('id') for ie in self.ielements]
...@@ -2018,40 +2048,39 @@ class ImageResponse(LoncapaResponse): ...@@ -2018,40 +2048,39 @@ class ImageResponse(LoncapaResponse):
correct_map = CorrectMap() correct_map = CorrectMap()
expectedset = self.get_mapped_answers() expectedset = self.get_mapped_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[ given = student_answers[aid] # This should be a string of the form '[x,y]'
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(r'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) acoords = re.match(r'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m: if not acoords:
raise Exception('[capamodule.capa.responsetypes.imageinput] ' raise Exception('[capamodule.capa.responsetypes.imageinput] '
'error grading %s (input=%s)' % (aid, given)) 'error grading {0} (input={1})'.format(aid, given))
(gx, gy) = [int(x) for x in m.groups()] (ans_x, ans_y) = [int(x) for x in acoords.groups()]
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 # Check whether given point lies in any of the solution
# rectangles # 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( sr_coords = re.match(
r'[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', r'[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
solution_rectangle.strip().replace(' ', '')) solution_rectangle.strip().replace(' ', ''))
if not m: if not sr_coords:
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( raise Exception(
'[capamodule.capa.responsetypes.imageinput] ' + msg) '[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 sr_coords.groups()]
# answer is correct if (x,y) is within the specified # answer is correct if (x,y) is within the specified
# rectangle # rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury): if (llx <= ans_x <= urx) and (lly <= ans_y <= ury):
correct_map.set(aid, 'correct') correct_map.set(aid, 'correct')
break break
if correct_map[aid]['correctness'] != 'correct' and regions[aid]: if correct_map[aid]['correctness'] != 'correct' and regions[aid]:
...@@ -2065,13 +2094,13 @@ class ImageResponse(LoncapaResponse): ...@@ -2065,13 +2094,13 @@ class ImageResponse(LoncapaResponse):
for region in parsed_region: for region in parsed_region:
polygon = MultiPoint(region).convex_hull polygon = MultiPoint(region).convex_hull
if (polygon.type == 'Polygon' and if (polygon.type == 'Polygon' and
polygon.contains(Point(gx, gy))): polygon.contains(Point(ans_x, ans_y))):
correct_map.set(aid, 'correct') correct_map.set(aid, 'correct')
break break
return correct_map return correct_map
def get_mapped_answers(self): def get_mapped_answers(self):
''' """
Returns the internal representation of the answers Returns the internal representation of the answers
Input: Input:
...@@ -2080,7 +2109,7 @@ class ImageResponse(LoncapaResponse): ...@@ -2080,7 +2109,7 @@ class ImageResponse(LoncapaResponse):
tuple (dict, dict) - tuple (dict, dict) -
rectangles (dict) - a map of inputs to the defined rectangle for that input rectangles (dict) - a map of inputs to the defined rectangle for that input
regions (dict) - a map of inputs to the defined region for that input regions (dict) - a map of inputs to the defined region for that input
''' """
answers = ( answers = (
dict([(ie.get('id'), ie.get( dict([(ie.get('id'), ie.get(
'rectangle')) for ie in self.ielements]), 'rectangle')) for ie in self.ielements]),
...@@ -2088,7 +2117,7 @@ class ImageResponse(LoncapaResponse): ...@@ -2088,7 +2117,7 @@ class ImageResponse(LoncapaResponse):
return answers return answers
def get_answers(self): def get_answers(self):
''' """
Returns the external representation of the answers Returns the external representation of the answers
Input: Input:
...@@ -2096,11 +2125,11 @@ class ImageResponse(LoncapaResponse): ...@@ -2096,11 +2125,11 @@ class ImageResponse(LoncapaResponse):
Returns: Returns:
dict (str, (str, str)) - a map of inputs to a tuple of their rectange dict (str, (str, str)) - a map of inputs to a tuple of their rectange
and their regions and their regions
''' """
answers = {} answers = {}
for ie in self.ielements: for ielt in self.ielements:
ie_id = ie.get('id') ie_id = ielt.get('id')
answers[ie_id] = (ie.get('rectangle'), ie.get('regions')) answers[ie_id] = (ielt.get('rectangle'), ielt.get('regions'))
return answers return answers
...@@ -2108,26 +2137,32 @@ class ImageResponse(LoncapaResponse): ...@@ -2108,26 +2137,32 @@ class ImageResponse(LoncapaResponse):
class AnnotationResponse(LoncapaResponse): class AnnotationResponse(LoncapaResponse):
''' """
Checking of annotation responses. Checking of annotation responses.
The response contains both a comment (student commentary) and an option (student tag). The response contains both a comment (student commentary) and an option (student tag).
Only the tag is currently graded. Answers may be incorrect, partially correct, or correct. Only the tag is currently graded. Answers may be incorrect, partially correct, or correct.
''' """
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 __init__(self, *args, **kwargs):
self.scoring_map = {}
self.answer_map = {}
super(AnnotationResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
xml = self.xml
self.scoring_map = self._get_scoring_map() self.scoring_map = self._get_scoring_map()
self.answer_map = self._get_answer_map() self.answer_map = self._get_answer_map()
self.maxpoints = self._get_max_points() self.maxpoints = self._get_max_points()
def get_score(self, student_answers): def get_score(self, student_answers):
''' Returns a CorrectMap for the student answer, which may include """
partially correct answers.''' Returns a CorrectMap for the student answer, which may include
partially correct answers.
"""
student_answer = student_answers[self.answer_id] student_answer = student_answers[self.answer_id]
student_option = self._get_submitted_option_id(student_answer) student_option = self._get_submitted_option_id(student_answer)
...@@ -2146,23 +2181,26 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2146,23 +2181,26 @@ class AnnotationResponse(LoncapaResponse):
return self.answer_map return self.answer_map
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:
option_scoring = dict([(option['id'], { option_scoring = dict([(
option['id'],
{
'correctness': choices.get(option['choice']), 'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice']) 'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield)]) }
) for option in self._find_options(inputfield)])
scoring_map[inputfield.get('id')] = option_scoring scoring_map[inputfield.get('id')] = option_scoring
return scoring_map return scoring_map
def _get_answer_map(self): def _get_answer_map(self):
''' 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( correct_option = self._find_option_with_choice(
...@@ -2173,13 +2211,13 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2173,13 +2211,13 @@ class AnnotationResponse(LoncapaResponse):
return answer_map return answer_map
def _get_max_points(self): def _get_max_points(self):
''' Returns a dict of the max points for each input: input id -> maxpoints. ''' """Returns a dict of the max points for each input: input id -> maxpoints."""
scoring = self.default_scoring scoring = self.default_scoring
correct_points = scoring.get('correct') correct_points = scoring.get('correct')
return dict([(inputfield.get('id'), correct_points) for inputfield in self.inputfields]) return dict([(inputfield.get('id'), correct_points) for inputfield in self.inputfields])
def _find_options(self, inputfield): def _find_options(self, inputfield):
''' Returns an array of dicts where each dict represents an option. ''' """Returns an array of dicts where each dict represents an option. """
elements = inputfield.findall('./options/option') elements = inputfield.findall('./options/option')
return [{ return [{
'id': index, 'id': index,
...@@ -2188,22 +2226,22 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2188,22 +2226,22 @@ class AnnotationResponse(LoncapaResponse):
} 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. """
for option in self._find_options(inputfield): for option in self._find_options(inputfield):
if option['choice'] == choice: if option['choice'] == choice:
return option return option
def _unpack(self, json_value): def _unpack(self, json_value):
''' Unpacks a student response value submitted as JSON.''' """Unpacks a student response value submitted as JSON."""
d = json.loads(json_value) json_d = json.loads(json_value)
if type(d) != dict: if type(json_d) != dict:
d = {} json_d = {}
comment_value = d.get('comment', '') comment_value = json_d.get('comment', '')
if not isinstance(d, basestring): if not isinstance(json_d, basestring):
comment_value = '' comment_value = ''
options_value = d.get('options', []) options_value = json_d.get('options', [])
if not isinstance(options_value, list): if not isinstance(options_value, list):
options_value = [] options_value = []
...@@ -2213,7 +2251,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2213,7 +2251,7 @@ class AnnotationResponse(LoncapaResponse):
} }
def _get_submitted_option_id(self, student_answer): def _get_submitted_option_id(self, student_answer):
''' Return the single option that was selected, otherwise None.''' """Return the single option that was selected, otherwise None."""
submitted = self._unpack(student_answer) submitted = self._unpack(student_answer)
option_ids = submitted['options_value'] option_ids = submitted['options_value']
if len(option_ids) == 1: if len(option_ids) == 1:
...@@ -2235,6 +2273,12 @@ class ChoiceTextResponse(LoncapaResponse): ...@@ -2235,6 +2273,12 @@ class ChoiceTextResponse(LoncapaResponse):
'radiotextgroup' 'radiotextgroup'
] ]
def __init__(self, *args, **kwargs):
self.correct_inputs = {}
self.answer_values = {}
self.correct_choices = {}
super(ChoiceTextResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
""" """
Sets up three dictionaries for use later: Sets up three dictionaries for use later:
...@@ -2250,10 +2294,8 @@ class ChoiceTextResponse(LoncapaResponse): ...@@ -2250,10 +2294,8 @@ class ChoiceTextResponse(LoncapaResponse):
""" """
context = self.context context = self.context
self.correct_choices = {}
self.assign_choice_names()
self.correct_inputs = {}
self.answer_values = {self.answer_id: []} self.answer_values = {self.answer_id: []}
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'))
for node in correct_xml: for node in correct_xml:
...@@ -2552,6 +2594,7 @@ class ChoiceTextResponse(LoncapaResponse): ...@@ -2552,6 +2594,7 @@ class ChoiceTextResponse(LoncapaResponse):
# TEMPORARY: List of all response subclasses # TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration # FIXME: To be replaced by auto-registration
# pylint: disable=E0604
__all__ = [CodeResponse, __all__ = [CodeResponse,
NumericalResponse, NumericalResponse,
FormulaResponse, FormulaResponse,
......
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