Commit b88c6b8d by Felix Sun

Hinter now works with formula responses. Tests broken.

parent c34a81a8
...@@ -1824,7 +1824,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1824,7 +1824,7 @@ class FormulaResponse(LoncapaResponse):
log.debug('formularesponse: error %s in formula', err) log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" % raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(answer)) cgi.escape(answer))
return tuple(out) return out
def randomize_variables(self, samples): def randomize_variables(self, samples):
""" """
......
...@@ -16,6 +16,8 @@ from xmodule.x_module import XModule ...@@ -16,6 +16,8 @@ from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, String, Integer, Boolean, Dict, List from xblock.core import Scope, String, Integer, Boolean, Dict, List
from capa.responsetypes import FormulaResponse, StudentInputError
from django.utils.html import escape from django.utils.html import escape
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -37,6 +39,14 @@ class CrowdsourceHinterFields(object): ...@@ -37,6 +39,14 @@ class CrowdsourceHinterFields(object):
mod_queue = Dict(help='A dictionary containing hints still awaiting approval', scope=Scope.content, mod_queue = Dict(help='A dictionary containing hints still awaiting approval', scope=Scope.content,
default={}) default={})
hint_pk = Integer(help='Used to index hints.', scope=Scope.content, default=0) 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. # 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 # 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. # None if the hint was not given.
...@@ -68,6 +78,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -68,6 +78,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
XModule.__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): def get_html(self):
""" """
...@@ -98,15 +117,45 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -98,15 +117,45 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return out 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. of the answer.
-Lon-capa dependent. -Lon-capa dependent.
-Assumes that the problem only has one part. -Assumes that the problem only has one part.
""" """
return str(float(answer.values()[0])) 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): def handle_ajax(self, dispatch, data):
""" """
This is the landing method for AJAX calls. This is the landing method for AJAX calls.
...@@ -134,44 +183,46 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -134,44 +183,46 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Called by hinter javascript after a problem is graded as incorrect. Called by hinter javascript after a problem is graded as incorrect.
Args: Args:
`data` -- must be interpretable by capa_answer_to_str. `data` -- must be interpretable by answer_to_str.
Output keys: Output keys:
- 'best_hint' is the hint text with the most votes. - '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`. - 'rand_hint_1' and 'rand_hint_2' are two random hints to the answer in `data`.
- 'answer' is the parsed answer that was submitted. - 'answer' is the parsed answer that was submitted.
""" """
try: try:
answer = self.capa_answer_to_str(data) answer = self.answer_to_str(data)
except ValueError: except ValueError:
# Sometimes, we get an answer that's just not parsable. Do nothing. # Sometimes, we get an answer that's just not parsable. Do nothing.
log.exception('Answer not parsable: ' + str(data)) log.exception('Answer not parsable: ' + str(data))
return return
# Make a signature of the answer, for formula responses.
signature = self.answer_signature(answer)
# Look for a hint to give. # Look for a hint to give.
# Make a local copy of self.hints - this means we only need to do one json unpacking. # 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.) # (This is because xblocks storage makes the following command a deep copy.)
local_hints = self.hints 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. # No hints to give. Return.
self.previous_answers += [[answer, [None, None, None]]] self.previous_answers += [[answer, [None, None, None]]]
return return
# Get the top hint, plus two random hints. # Get the top hint, plus two random hints.
n_hints = len(local_hints[answer]) n_hints = len(local_hints[signature])
best_hint_index = max(local_hints[answer], key=lambda key: local_hints[answer][key][1]) best_hint_index = max(local_hints[signature], key=lambda key: local_hints[signature][key][1])
best_hint = local_hints[answer][best_hint_index][0] best_hint = local_hints[signature][best_hint_index][0]
if len(local_hints[answer]) == 1: if len(local_hints[signature]) == 1:
rand_hint_1 = '' rand_hint_1 = ''
rand_hint_2 = '' rand_hint_2 = ''
self.previous_answers += [[answer, [best_hint_index, None, None]]] self.previous_answers += [[answer, [best_hint_index, None, None]]]
elif n_hints == 2: elif n_hints == 2:
best_hint = local_hints[answer].values()[0][0] best_hint = local_hints[signature].values()[0][0]
best_hint_index = local_hints[answer].keys()[0] best_hint_index = local_hints[signature].keys()[0]
rand_hint_1 = local_hints[answer].values()[1][0] rand_hint_1 = local_hints[signature].values()[1][0]
hint_index_1 = local_hints[answer].keys()[1] hint_index_1 = local_hints[signature].keys()[1]
rand_hint_2 = '' rand_hint_2 = ''
self.previous_answers += [[answer, [best_hint_index, hint_index_1, None]]] self.previous_answers += [[answer, [best_hint_index, hint_index_1, None]]]
else: else:
(hint_index_1, rand_hint_1), (hint_index_2, rand_hint_2) =\ (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_1 = rand_hint_1[0]
rand_hint_2 = rand_hint_2[0] rand_hint_2 = rand_hint_2[0]
self.previous_answers += [[answer, [best_hint_index, hint_index_1, hint_index_2]]] self.previous_answers += [[answer, [best_hint_index, hint_index_1, hint_index_2]]]
...@@ -206,12 +257,13 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -206,12 +257,13 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
answer, hints_offered = self.previous_answers[i] answer, hints_offered = self.previous_answers[i]
if answer not in answer_to_hints: if answer not in answer_to_hints:
answer_to_hints[answer] = {} 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 # Go through each hint, and add to index_to_hints
for hint_id in hints_offered: 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: 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: except KeyError:
# Sometimes, the hint that a user saw will have been deleted by the instructor. # Sometimes, the hint that a user saw will have been deleted by the instructor.
continue continue
...@@ -234,11 +286,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -234,11 +286,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
if self.user_voted: if self.user_voted:
return json.dumps({'contents': 'Sorry, but you have already voted!'}) return json.dumps({'contents': 'Sorry, but you have already voted!'})
ans = data['answer'] ans = data['answer']
signature = self.answer_signature(ans)
hint_pk = str(data['hint']) hint_pk = str(data['hint'])
pk_list = json.loads(data['pk_list']) pk_list = json.loads(data['pk_list'])
# We use temp_dict because we need to do a direct write for the database to update. # We use temp_dict because we need to do a direct write for the database to update.
temp_dict = self.hints temp_dict = self.hints
temp_dict[ans][hint_pk][1] += 1 temp_dict[signature][hint_pk][1] += 1
self.hints = temp_dict self.hints = temp_dict
# Don't let the user vote again! # Don't let the user vote again!
self.user_voted = True self.user_voted = True
...@@ -246,7 +299,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -246,7 +299,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# Return a list of how many votes each hint got. # Return a list of how many votes each hint got.
hint_and_votes = [] hint_and_votes = []
for vote_pk in pk_list: 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) hint_and_votes.sort(key=lambda pair: pair[1], reverse=True)
# Reset self.previous_answers. # Reset self.previous_answers.
...@@ -266,6 +319,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -266,6 +319,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well. # Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint = escape(data['hint']) hint = escape(data['hint'])
answer = data['answer'] answer = data['answer']
signature = self.answer_signature(answer)
# Only allow a student to vote or submit a hint once. # Only allow a student to vote or submit a hint once.
if self.user_voted: if self.user_voted:
return {'message': 'Sorry, but you have already voted!'} return {'message': 'Sorry, but you have already voted!'}
...@@ -276,9 +330,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -276,9 +330,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
else: else:
temp_dict = self.hints temp_dict = self.hints
if answer in temp_dict: 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: 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 self.hint_pk += 1
if self.moderate == 'True': if self.moderate == 'True':
self.mod_queue = temp_dict 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