Hinter now works with formula responses. Tests broken.
This commit is contained in:
@@ -1824,7 +1824,7 @@ class FormulaResponse(LoncapaResponse):
|
||||
log.debug('formularesponse: error %s in formula', err)
|
||||
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
|
||||
cgi.escape(answer))
|
||||
return tuple(out)
|
||||
return out
|
||||
|
||||
def randomize_variables(self, samples):
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,8 @@ from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xblock.core import Scope, String, Integer, Boolean, Dict, List
|
||||
|
||||
from capa.responsetypes import FormulaResponse, StudentInputError
|
||||
|
||||
from django.utils.html import escape
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -37,6 +39,14 @@ class CrowdsourceHinterFields(object):
|
||||
mod_queue = Dict(help='A dictionary containing hints still awaiting approval', scope=Scope.content,
|
||||
default={})
|
||||
hint_pk = Integer(help='Used to index hints.', scope=Scope.content, default=0)
|
||||
# signature_to_ans maps an answer signature to an answer string that shows that answer in a
|
||||
# human-readable form.
|
||||
signature_to_ans = Dict(help='Maps a signature to a representative formula.', scope=Scope.content,
|
||||
default={})
|
||||
# A list of dictionaries, each of which represents an n-dimenstional point that we plug into
|
||||
# formulas. Each dictionary maps variables to values, eg {'x': 5.1}.
|
||||
formula_test_values = List(help='The values that we plug into formula responses', scope=Scope.content,
|
||||
default=[])
|
||||
# A list of previous answers this student made to this problem.
|
||||
# Of the form [answer, [hint_pk_1, hint_pk_2, hint_pk_3]] for each problem. hint_pk's are
|
||||
# None if the hint was not given.
|
||||
@@ -68,6 +78,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
XModule.__init__(self, *args, **kwargs)
|
||||
# We need to know whether we are working with a FormulaResponse problem.
|
||||
self.is_formula = (type(self.get_display_items()[0].lcp.responders.values()[0]) == FormulaResponse)
|
||||
if self.is_formula:
|
||||
self.answer_to_str = self.formula_answer_to_str
|
||||
self.answer_signature = self.formula_answer_signature
|
||||
else:
|
||||
self.answer_to_str = self.numerical_answer_to_str
|
||||
# Right now, numerical problems don't need special answer signature treatment.
|
||||
self.answer_signature = lambda x: x
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
@@ -98,15 +117,45 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
|
||||
return out
|
||||
|
||||
def capa_answer_to_str(self, answer):
|
||||
def numerical_answer_to_str(self, answer):
|
||||
"""
|
||||
Converts capa answer format to a string representation
|
||||
Converts capa numerical answer format to a string representation
|
||||
of the answer.
|
||||
-Lon-capa dependent.
|
||||
-Assumes that the problem only has one part.
|
||||
"""
|
||||
return str(float(answer.values()[0]))
|
||||
|
||||
def formula_answer_to_str(self, answer):
|
||||
"""
|
||||
Converts capa formula answer into a string.
|
||||
-Lon-capa dependent.
|
||||
-Assumes that the problem only has one part.
|
||||
"""
|
||||
return str(answer.values()[0])
|
||||
|
||||
def formula_answer_signature(self, answer):
|
||||
"""
|
||||
Converts a capa answer string (output of formula_answer_to_str)
|
||||
to a string unique to each formula equality class.
|
||||
So, x^2 and x*x would have the same signature, which would differ
|
||||
from the signature of 2*x^2.
|
||||
"""
|
||||
responder = self.get_display_items()[0].lcp.responders.values()[0]
|
||||
if self.formula_test_values == []:
|
||||
# Make a set of test values, and save them.
|
||||
self.formula_test_values = responder.randomize_variables(responder.samples)
|
||||
try:
|
||||
# TODO, maybe: add some rounding to signature generation, so that floating point
|
||||
# errors don't make a difference.
|
||||
out = str(responder.hash_answers(answer, self.formula_test_values))
|
||||
except StudentInputError:
|
||||
# I'm not sure what's the best thing to do here.
|
||||
# I'll return the empty string, for now.
|
||||
# That way, all invalid hints are clustered together.
|
||||
return ''
|
||||
return out
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
"""
|
||||
This is the landing method for AJAX calls.
|
||||
@@ -134,44 +183,46 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
|
||||
Called by hinter javascript after a problem is graded as incorrect.
|
||||
Args:
|
||||
`data` -- must be interpretable by capa_answer_to_str.
|
||||
`data` -- must be interpretable by answer_to_str.
|
||||
Output keys:
|
||||
- 'best_hint' is the hint text with the most votes.
|
||||
- 'rand_hint_1' and 'rand_hint_2' are two random hints to the answer in `data`.
|
||||
- 'answer' is the parsed answer that was submitted.
|
||||
"""
|
||||
try:
|
||||
answer = self.capa_answer_to_str(data)
|
||||
answer = self.answer_to_str(data)
|
||||
except ValueError:
|
||||
# Sometimes, we get an answer that's just not parsable. Do nothing.
|
||||
log.exception('Answer not parsable: ' + str(data))
|
||||
return
|
||||
# Make a signature of the answer, for formula responses.
|
||||
signature = self.answer_signature(answer)
|
||||
# Look for a hint to give.
|
||||
# Make a local copy of self.hints - this means we only need to do one json unpacking.
|
||||
# (This is because xblocks storage makes the following command a deep copy.)
|
||||
local_hints = self.hints
|
||||
if (answer not in local_hints) or (len(local_hints[answer]) == 0):
|
||||
if (signature not in local_hints) or (len(local_hints[signature]) == 0):
|
||||
# No hints to give. Return.
|
||||
self.previous_answers += [[answer, [None, None, None]]]
|
||||
return
|
||||
# Get the top hint, plus two random hints.
|
||||
n_hints = len(local_hints[answer])
|
||||
best_hint_index = max(local_hints[answer], key=lambda key: local_hints[answer][key][1])
|
||||
best_hint = local_hints[answer][best_hint_index][0]
|
||||
if len(local_hints[answer]) == 1:
|
||||
n_hints = len(local_hints[signature])
|
||||
best_hint_index = max(local_hints[signature], key=lambda key: local_hints[signature][key][1])
|
||||
best_hint = local_hints[signature][best_hint_index][0]
|
||||
if len(local_hints[signature]) == 1:
|
||||
rand_hint_1 = ''
|
||||
rand_hint_2 = ''
|
||||
self.previous_answers += [[answer, [best_hint_index, None, None]]]
|
||||
elif n_hints == 2:
|
||||
best_hint = local_hints[answer].values()[0][0]
|
||||
best_hint_index = local_hints[answer].keys()[0]
|
||||
rand_hint_1 = local_hints[answer].values()[1][0]
|
||||
hint_index_1 = local_hints[answer].keys()[1]
|
||||
best_hint = local_hints[signature].values()[0][0]
|
||||
best_hint_index = local_hints[signature].keys()[0]
|
||||
rand_hint_1 = local_hints[signature].values()[1][0]
|
||||
hint_index_1 = local_hints[signature].keys()[1]
|
||||
rand_hint_2 = ''
|
||||
self.previous_answers += [[answer, [best_hint_index, hint_index_1, None]]]
|
||||
else:
|
||||
(hint_index_1, rand_hint_1), (hint_index_2, rand_hint_2) =\
|
||||
random.sample(local_hints[answer].items(), 2)
|
||||
random.sample(local_hints[signature].items(), 2)
|
||||
rand_hint_1 = rand_hint_1[0]
|
||||
rand_hint_2 = rand_hint_2[0]
|
||||
self.previous_answers += [[answer, [best_hint_index, hint_index_1, hint_index_2]]]
|
||||
@@ -206,12 +257,13 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
answer, hints_offered = self.previous_answers[i]
|
||||
if answer not in answer_to_hints:
|
||||
answer_to_hints[answer] = {}
|
||||
if answer in self.hints:
|
||||
signature = self.answer_signature(answer)
|
||||
if signature in self.hints:
|
||||
# Go through each hint, and add to index_to_hints
|
||||
for hint_id in hints_offered:
|
||||
if (hint_id is not None) and (hint_id not in answer_to_hints[answer]):
|
||||
if (hint_id is not None) and (hint_id not in answer_to_hints[signature]):
|
||||
try:
|
||||
answer_to_hints[answer][hint_id] = self.hints[answer][str(hint_id)][0]
|
||||
answer_to_hints[answer][hint_id] = self.hints[signature][str(hint_id)][0]
|
||||
except KeyError:
|
||||
# Sometimes, the hint that a user saw will have been deleted by the instructor.
|
||||
continue
|
||||
@@ -234,11 +286,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
if self.user_voted:
|
||||
return json.dumps({'contents': 'Sorry, but you have already voted!'})
|
||||
ans = data['answer']
|
||||
signature = self.answer_signature(ans)
|
||||
hint_pk = str(data['hint'])
|
||||
pk_list = json.loads(data['pk_list'])
|
||||
# We use temp_dict because we need to do a direct write for the database to update.
|
||||
temp_dict = self.hints
|
||||
temp_dict[ans][hint_pk][1] += 1
|
||||
temp_dict[signature][hint_pk][1] += 1
|
||||
self.hints = temp_dict
|
||||
# Don't let the user vote again!
|
||||
self.user_voted = True
|
||||
@@ -246,7 +299,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
# Return a list of how many votes each hint got.
|
||||
hint_and_votes = []
|
||||
for vote_pk in pk_list:
|
||||
hint_and_votes.append(temp_dict[ans][str(vote_pk)])
|
||||
hint_and_votes.append(temp_dict[signature][str(vote_pk)])
|
||||
|
||||
hint_and_votes.sort(key=lambda pair: pair[1], reverse=True)
|
||||
# Reset self.previous_answers.
|
||||
@@ -266,6 +319,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
|
||||
hint = escape(data['hint'])
|
||||
answer = data['answer']
|
||||
signature = self.answer_signature(answer)
|
||||
# Only allow a student to vote or submit a hint once.
|
||||
if self.user_voted:
|
||||
return {'message': 'Sorry, but you have already voted!'}
|
||||
@@ -276,9 +330,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
else:
|
||||
temp_dict = self.hints
|
||||
if answer in temp_dict:
|
||||
temp_dict[answer][str(self.hint_pk)] = [hint, 1] # With one vote (the user himself).
|
||||
temp_dict[signature][str(self.hint_pk)] = [hint, 1] # With one vote (the user himself).
|
||||
else:
|
||||
temp_dict[answer] = {str(self.hint_pk): [hint, 1]}
|
||||
temp_dict[signature] = {str(self.hint_pk): [hint, 1]}
|
||||
# Add the signature to signature_to_ans, if it's not there yet.
|
||||
# This allows instructors to see a human-readable answer that corresponds to each signature.
|
||||
if answer not in self.signature_to_ans:
|
||||
local_sta = self.signature_to_ans
|
||||
local_sta[signature] = answer
|
||||
self.signature_to_ans = local_sta
|
||||
self.hint_pk += 1
|
||||
if self.moderate == 'True':
|
||||
self.mod_queue = temp_dict
|
||||
|
||||
Reference in New Issue
Block a user