Commit a730f918 by Felix Sun

Fixed numerous 500 errors that result from mal-formatted post requests.

Fixed tests to work with symbolic answer changes.
parent 199b6325
...@@ -173,6 +173,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -173,6 +173,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
if out is None: if out is None:
out = {'op': 'empty'} out = {'op': 'empty'}
elif 'error' in out:
# Error in processing.
out.update({'op': 'error'})
else: else:
out.update({'op': dispatch}) out.update({'op': dispatch})
return json.dumps({'contents': self.system.render_template('hinter_display.html', out)}) return json.dumps({'contents': self.system.render_template('hinter_display.html', out)})
...@@ -288,14 +291,23 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -288,14 +291,23 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs. Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs.
""" """
if self.user_voted: if self.user_voted:
return json.dumps({'contents': 'Sorry, but you have already voted!'}) return {'error': 'Sorry, but you have already voted!'}
ans = data['answer'] ans = data['answer']
signature = self.answer_signature(ans) signature = self.answer_signature(ans)
if signature is None:
# Uh oh. Invalid answer.
log.exception('Failure in hinter tally_vote: Unable to parse answer: ' + ans)
return {'error': 'Failure in voting!'}
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[signature][hint_pk][1] += 1 try:
temp_dict[signature][hint_pk][1] += 1
except KeyError:
log.exception('Failure in hinter tally_vote: User voted for non-existant hint: Answer=' +
ans + ' pk=' + hint_pk)
return {'error': 'Failure in voting!'}
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
...@@ -303,7 +315,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -303,7 +315,10 @@ 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[signature][str(vote_pk)]) try:
hint_and_votes.append(temp_dict[signature][str(vote_pk)])
except KeyError:
log.exception('In hinter tally_vote: pk_list contains non-existant pk: ' + 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.
...@@ -324,6 +339,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -324,6 +339,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
hint = escape(data['hint']) hint = escape(data['hint'])
answer = data['answer'] answer = data['answer']
signature = self.answer_signature(answer) signature = self.answer_signature(answer)
if signature is None:
log.exception('Failure in hinter submit_hint: Unable to parse answer: ' + answer)
return {'error': 'Could not submit 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!'}
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
Tests the crowdsourced hinter xmodule. Tests the crowdsourced hinter xmodule.
""" """
from mock import Mock from mock import Mock, MagicMock
import unittest import unittest
import copy import copy
from xmodule.crowdsource_hinter import CrowdsourceHinterModule from xmodule.crowdsource_hinter import CrowdsourceHinterModule
from xmodule.vertical_module import VerticalModule, VerticalDescriptor from xmodule.vertical_module import VerticalModule, VerticalDescriptor
from capa.responsetypes import StudentInputError
from . import get_test_system from . import get_test_system
import json import json
...@@ -94,12 +96,54 @@ class CHModuleFactory(object): ...@@ -94,12 +96,54 @@ class CHModuleFactory(object):
if moderate is not None: if moderate is not None:
model_data['moderate'] = moderate model_data['moderate'] = moderate
descriptor = Mock(weight="1") descriptor = Mock(weight='1')
# Make the descriptor have a capa problem child.
capa_descriptor = MagicMock()
capa_descriptor.name = 'capa'
descriptor.get_children = lambda: [capa_descriptor]
# Make a fake capa module.
capa_module = MagicMock()
capa_module.responders = {'responder0': MagicMock()}
capa_module.displayable_items = lambda: [capa_module]
system = get_test_system() system = get_test_system()
# Make the system have a marginally-functional get_module
def fake_get_module(descriptor):
"""
A fake module-maker.
"""
if descriptor.name == 'capa':
return capa_module
system.get_module = fake_get_module
module = CrowdsourceHinterModule(system, descriptor, model_data) module = CrowdsourceHinterModule(system, descriptor, model_data)
return module return module
@staticmethod
def setup_formula_response(module):
"""
Adds additional mock methods to the module, so that we can test
formula responses.
"""
# Do a bunch of monkey patching, to mock the lon-capa problem.
responder = MagicMock()
responder.randomize_variables = MagicMock(return_value=4)
def fake_hash_answers(answer, test_values):
""" A fake answer hasher """
if test_values == 4 and answer == 'x*y^2':
return 'good'
raise StudentInputError
responder.hash_answers = fake_hash_answers
lcp = MagicMock()
lcp.responders = {'responder0': responder}
module.get_display_items()[0].lcp = lcp
return module
class VerticalWithModulesFactory(object): class VerticalWithModulesFactory(object):
""" """
...@@ -226,6 +270,44 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -226,6 +270,44 @@ class CrowdsourceHinterTest(unittest.TestCase):
self.assertTrue('Test numerical problem.' in out_html) self.assertTrue('Test numerical problem.' in out_html)
self.assertTrue('Another test numerical problem.' in out_html) self.assertTrue('Another test numerical problem.' in out_html)
def test_numerical_answer_to_str(self):
"""
Tests the get request to string converter for numerical responses.
"""
mock_module = CHModuleFactory.create()
get = {'response1': '4'}
parsed = mock_module.numerical_answer_to_str(get)
self.assertTrue(parsed == '4.0')
def test_formula_answer_to_str(self):
"""
Tests the get request to string converter for formula responses.
"""
mock_module = CHModuleFactory.create()
get = {'response1': 'x*y^2'}
parsed = mock_module.formula_answer_to_str(get)
self.assertTrue(parsed == 'x*y^2')
def test_formula_answer_signature(self):
"""
Tests the answer signature generator for formula responses.
"""
mock_module = CHModuleFactory.create()
mock_module = CHModuleFactory.setup_formula_response(mock_module)
answer = 'x*y^2'
out = mock_module.formula_answer_signature(answer)
self.assertTrue(out == 'good')
def test_formula_answer_signature_failure(self):
"""
Makes sure that bad answer strings return None as a signature.
"""
mock_module = CHModuleFactory.create()
mock_module = CHModuleFactory.setup_formula_response(mock_module)
answer = 'fish'
out = mock_module.formula_answer_signature(answer)
self.assertTrue(out is None)
def test_gethint_0hint(self): def test_gethint_0hint(self):
""" """
Someone asks for a hint, when there's no hint to give. Someone asks for a hint, when there's no hint to give.
...@@ -343,6 +425,44 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -343,6 +425,44 @@ class CrowdsourceHinterTest(unittest.TestCase):
self.assertTrue(['Best hint', 40] in dict_out['hint_and_votes']) self.assertTrue(['Best hint', 40] in dict_out['hint_and_votes'])
self.assertTrue(['Another hint', 31] in dict_out['hint_and_votes']) self.assertTrue(['Another hint', 31] in dict_out['hint_and_votes'])
def test_vote_unparsable(self):
"""
A user somehow votes for an unparsable answer.
Should return a friendly error.
(This is an unusual exception path - I don't know how it occurs,
except if you manually make a post request. But, it seems to happen
occasionally.)
"""
mock_module = CHModuleFactory.create()
# None means that the answer couldn't be parsed.
mock_module.answer_signature = lambda text: None
json_in = {'answer': 'fish', 'hint': 3, 'pk_list': '[]'}
dict_out = mock_module.tally_vote(json_in)
print dict_out
self.assertTrue(dict_out == {'error': 'Failure in voting!'})
def test_vote_nohint(self):
"""
A user somehow votes for a hint that doesn't exist.
Should return a friendly error.
"""
mock_module = CHModuleFactory.create()
json_in = {'answer': '24.0', 'hint': '25', 'pk_list': '[]'}
dict_out = mock_module.tally_vote(json_in)
self.assertTrue(dict_out == {'error': 'Failure in voting!'})
def test_vote_badpklist(self):
"""
Some of the pk's specified in pk_list are invalid.
Should just skip those.
"""
mock_module = CHModuleFactory.create()
json_in = {'answer': '24.0', 'hint': '0', 'pk_list': json.dumps([0, 12])}
hint_and_votes = mock_module.tally_vote(json_in)['hint_and_votes']
self.assertTrue(['Best hint', 41] in hint_and_votes)
self.assertTrue(len(hint_and_votes) == 1)
def test_submithint_nopermission(self): def test_submithint_nopermission(self):
""" """
A user tries to submit a hint, but he has already voted. A user tries to submit a hint, but he has already voted.
...@@ -399,6 +519,17 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -399,6 +519,17 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module.submit_hint(json_in) mock_module.submit_hint(json_in)
self.assertTrue(mock_module.hints['29.0']['0'][0] == u'<script> alert("Trololo"); </script>') self.assertTrue(mock_module.hints['29.0']['0'][0] == u'<script> alert("Trololo"); </script>')
def test_submithint_unparsable(self):
mock_module = CHModuleFactory.create()
mock_module.answer_signature = lambda text: None
json_in = {'answer': 'fish', 'hint': 'A hint'}
dict_out = mock_module.submit_hint(json_in)
print dict_out
print mock_module.hints
self.assertTrue('error' in dict_out)
self.assertTrue(None not in mock_module.hints)
self.assertTrue('fish' not in mock_module.hints)
def test_template_gethint(self): def test_template_gethint(self):
""" """
Test the templates for get_hint. Test the templates for get_hint.
......
...@@ -137,6 +137,10 @@ What would you say to help someone who got this wrong answer? ...@@ -137,6 +137,10 @@ What would you say to help someone who got this wrong answer?
${simple_message()} ${simple_message()}
% endif % endif
% if op == "error":
${error}
% endif
% if op == "vote": % if op == "vote":
${show_votes()} ${show_votes()}
% endif % endif
......
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