Commit b88c6b8d by Felix Sun

Hinter now works with formula responses. Tests broken.

parent c34a81a8
......@@ -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
......
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