Commit 6b40c5cf by Felix Sun

Changed hint voting ui to show all hints on one page.

Fixed broken tests for hint manager.
parent a730f918
...@@ -18,6 +18,9 @@ from xblock.core import Scope, String, Integer, Boolean, Dict, List ...@@ -18,6 +18,9 @@ from xblock.core import Scope, String, Integer, Boolean, Dict, List
from capa.responsetypes import FormulaResponse, StudentInputError from capa.responsetypes import FormulaResponse, StudentInputError
from calc import evaluator, UndefinedVariable
from pyparsing import ParseException
from django.utils.html import escape from django.utils.html import escape
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -85,8 +88,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -85,8 +88,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
self.answer_signature = self.formula_answer_signature self.answer_signature = self.formula_answer_signature
else: else:
self.answer_to_str = self.numerical_answer_to_str self.answer_to_str = self.numerical_answer_to_str
# Right now, numerical problems don't need special answer signature treatment. self.answer_signature = self.numerical_answer_signature
self.answer_signature = lambda x: x
def get_html(self): def get_html(self):
""" """
...@@ -134,6 +136,17 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -134,6 +136,17 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
""" """
return str(answer.values()[0]) return str(answer.values()[0])
def numerical_answer_signature(self, answer):
"""
Runs the answer string through the evaluator. (This is because
symbolic expressions like sin(pi/12)*3 are allowed.)
"""
try:
out = str(evaluator(dict(), dict(), answer))
except (UndefinedVariable, ParseException):
out = None
return out
def formula_answer_signature(self, answer): def formula_answer_signature(self, answer):
""" """
Converts a capa answer string (output of formula_answer_to_str) Converts a capa answer string (output of formula_answer_to_str)
...@@ -200,7 +213,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -200,7 +213,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return return
# Make a signature of the answer, for formula responses. # Make a signature of the answer, for formula responses.
signature = self.answer_signature(answer) signature = self.answer_signature(answer)
if signature == None: if signature is None:
# Sometimes, signature conversion may fail. # Sometimes, signature conversion may fail.
log.exception('Signature conversion failed: ' + str(answer)) log.exception('Signature conversion failed: ' + str(answer))
return return
...@@ -258,13 +271,21 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -258,13 +271,21 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# be allowed to make one vote / submission, but he can choose which wrong answer # be allowed to make one vote / submission, but he can choose which wrong answer
# he wants to look at. # he wants to look at.
answer_to_hints = {} # answer_to_hints[answer text][hint pk] -> hint text answer_to_hints = {} # answer_to_hints[answer text][hint pk] -> hint text
signature_to_ans = {} # Lets us combine equivalent answers
# Same mapping as the field, but local.
# Go through each previous answer, and populate index_to_hints and index_to_answer. # Go through each previous answer, and populate index_to_hints and index_to_answer.
for i in xrange(len(self.previous_answers)): for i in xrange(len(self.previous_answers)):
answer, hints_offered = self.previous_answers[i] answer, hints_offered = self.previous_answers[i]
# Does this answer equal one of the ones offered already?
signature = self.answer_signature(answer)
if signature in signature_to_ans:
# Re-assign this answer text to the one we've seen already.
answer = signature_to_ans[signature]
else:
signature_to_ans[signature] = answer
if answer not in answer_to_hints: if answer not in answer_to_hints:
answer_to_hints[answer] = {} answer_to_hints[answer] = {}
signature = self.answer_signature(answer)
if signature in self.hints: 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:
...@@ -285,9 +306,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -285,9 +306,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
`data` -- expected to have the following keys: `data` -- expected to have the following keys:
'answer': text of answer we're voting on 'answer': text of answer we're voting on
'hint': hint_pk 'hint': hint_pk
'pk_list': We will return a list of how many votes each hint has so far. 'pk_list': A list of [answer, pk] pairs, each of which representing a hint.
We will return a list of how many votes each hint in the list has so far.
It's up to the browser to specify which hints to return vote counts for. It's up to the browser to specify which hints to return vote counts for.
Every pk listed here will have a hint count returned.
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:
...@@ -299,7 +321,6 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -299,7 +321,6 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
log.exception('Failure in hinter tally_vote: Unable to parse answer: ' + ans) log.exception('Failure in hinter tally_vote: Unable to parse answer: ' + ans)
return {'error': 'Failure in voting!'} return {'error': 'Failure in voting!'}
hint_pk = str(data['hint']) 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. # 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
try: try:
...@@ -313,12 +334,18 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): ...@@ -313,12 +334,18 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
self.user_voted = True self.user_voted = True
# Return a list of how many votes each hint got. # Return a list of how many votes each hint got.
pk_list = json.loads(data['pk_list'])
hint_and_votes = [] hint_and_votes = []
for vote_pk in pk_list: for answer, vote_pk in pk_list:
signature = self.answer_signature(answer)
if signature is None:
log.exception('In hinter tally_vote, couldn\'t parse ' + answer)
continue
try: try:
hint_and_votes.append(temp_dict[signature][str(vote_pk)]) hint_and_votes.append(temp_dict[signature][str(vote_pk)])
except KeyError: except KeyError:
log.exception('In hinter tally_vote: pk_list contains non-existant pk: ' + str(vote_pk)) log.exception('In hinter tally_vote, couldn\'t find: '
+ answer + ', ' + 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.
......
...@@ -48,9 +48,7 @@ class @Hinter ...@@ -48,9 +48,7 @@ class @Hinter
vote: (eventObj) => vote: (eventObj) =>
target = @$(eventObj.currentTarget) target = @$(eventObj.currentTarget)
parent_div = $('.previous-answer[data-answer="'+target.attr('data-answer')+'"]') all_pks = @$('#pk-list').attr('data-pk-list')
all_pks = parent_div.attr('data-all-pks')
console.debug(all_pks)
post_json = {'answer': target.attr('data-answer'), 'hint': target.data('hintno'), 'pk_list': all_pks} post_json = {'answer': target.attr('data-answer'), 'hint': target.data('hintno'), 'pk_list': all_pks}
$.postWithPrefix "#{@url}/vote", post_json, (response) => $.postWithPrefix "#{@url}/vote", post_json, (response) =>
@render(response.contents) @render(response.contents)
...@@ -58,7 +56,6 @@ class @Hinter ...@@ -58,7 +56,6 @@ class @Hinter
submit_hint: (eventObj) => submit_hint: (eventObj) =>
target = @$(eventObj.currentTarget) target = @$(eventObj.currentTarget)
textarea = $('.custom-hint[data-answer="'+target.attr('data-answer')+'"]') textarea = $('.custom-hint[data-answer="'+target.attr('data-answer')+'"]')
console.debug(textarea)
post_json = {'answer': target.attr('data-answer'), 'hint': @$(textarea).val()} post_json = {'answer': target.attr('data-answer'), 'hint': @$(textarea).val()}
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) => $.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
@render(response.contents) @render(response.contents)
......
...@@ -288,6 +288,26 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -288,6 +288,26 @@ class CrowdsourceHinterTest(unittest.TestCase):
parsed = mock_module.formula_answer_to_str(get) parsed = mock_module.formula_answer_to_str(get)
self.assertTrue(parsed == 'x*y^2') self.assertTrue(parsed == 'x*y^2')
def test_numerical_answer_signature(self):
"""
Tests answer signature generator for numerical responses.
"""
mock_module = CHModuleFactory.create()
answer = '4*5+3'
signature = mock_module.numerical_answer_signature(answer)
print signature
self.assertTrue(signature == '23.0')
def test_numerical_answer_signature_failure(self):
"""
Makes sure that unparsable numerical answers return None.
"""
mock_module = CHModuleFactory.create()
answer = 'fish'
signature = mock_module.numerical_answer_signature(answer)
print signature
self.assertTrue(signature is None)
def test_formula_answer_signature(self): def test_formula_answer_signature(self):
""" """
Tests the answer signature generator for formula responses. Tests the answer signature generator for formula responses.
...@@ -406,7 +426,7 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -406,7 +426,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
Should not change any vote tallies. Should not change any vote tallies.
""" """
mock_module = CHModuleFactory.create(user_voted=True) mock_module = CHModuleFactory.create(user_voted=True)
json_in = {'answer': '24.0', 'hint': 1, 'pk_list': json.dumps([1, 3])} json_in = {'answer': '24.0', 'hint': 1, 'pk_list': json.dumps([['24.0', 1], ['24.0', 3]])}
old_hints = copy.deepcopy(mock_module.hints) old_hints = copy.deepcopy(mock_module.hints)
mock_module.tally_vote(json_in) mock_module.tally_vote(json_in)
self.assertTrue(mock_module.hints == old_hints) self.assertTrue(mock_module.hints == old_hints)
...@@ -418,7 +438,7 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -418,7 +438,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
""" """
mock_module = CHModuleFactory.create( mock_module = CHModuleFactory.create(
previous_answers=[['24.0', [0, 3, None]]]) previous_answers=[['24.0', [0, 3, None]]])
json_in = {'answer': '24.0', 'hint': 3, 'pk_list': json.dumps([0, 3])} json_in = {'answer': '24.0', 'hint': 3, 'pk_list': json.dumps([['24.0', 0], ['24.0', 3]])}
dict_out = mock_module.tally_vote(json_in) dict_out = mock_module.tally_vote(json_in)
self.assertTrue(mock_module.hints['24.0']['0'][1] == 40) self.assertTrue(mock_module.hints['24.0']['0'][1] == 40)
self.assertTrue(mock_module.hints['24.0']['3'][1] == 31) self.assertTrue(mock_module.hints['24.0']['3'][1] == 31)
...@@ -457,7 +477,7 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -457,7 +477,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
Should just skip those. Should just skip those.
""" """
mock_module = CHModuleFactory.create() mock_module = CHModuleFactory.create()
json_in = {'answer': '24.0', 'hint': '0', 'pk_list': json.dumps([0, 12])} json_in = {'answer': '24.0', 'hint': '0', 'pk_list': json.dumps([['24.0', 0], ['24.0', 12]])}
hint_and_votes = mock_module.tally_vote(json_in)['hint_and_votes'] hint_and_votes = mock_module.tally_vote(json_in)['hint_and_votes']
self.assertTrue(['Best hint', 41] in hint_and_votes) self.assertTrue(['Best hint', 41] in hint_and_votes)
self.assertTrue(len(hint_and_votes) == 1) self.assertTrue(len(hint_and_votes) == 1)
......
...@@ -18,11 +18,6 @@ ...@@ -18,11 +18,6 @@
</%def> </%def>
<%def name="get_feedback()"> <%def name="get_feedback()">
<p><em> Participation in the hinting system is strictly optional, and will not influence your grade. </em></p>
<p>
Help your classmates by writing hints for this problem. Start by picking one of your previous incorrect answers from below:
</p>
<% <%
def unspace(string): def unspace(string):
""" """
...@@ -30,7 +25,44 @@ ...@@ -30,7 +25,44 @@
removes spaces. removes spaces.
""" """
return ''.join(string.split()) return ''.join(string.split())
# Make a list of all hints shown. (This is fed back to the site as pk_list.)
# At the same time, determine whether any hints were shown at all.
# If the user never saw hints, don't ask him to vote.
import json
hints_exist = False
pk_list = []
for answer, pk_dict in answer_to_hints.items():
if len(pk_dict) > 0:
hints_exist = True
for pk, hint_text in pk_dict.items():
pk_list.append([answer, pk])
json_pk_list = json.dumps(pk_list)
%> %>
% if hints_exist:
<p>
<em> Optional. </em> Help us improve our hints! Which hint was most helpful to you?
</p>
<div id="pk-list" data-pk-list='${json_pk_list}' style="display:none"> </div>
% for answer, pk_dict in answer_to_hints.items():
% for hint_pk, hint_text in pk_dict.items():
<p>
<input class="vote" data-answer="${answer}" data-hintno="${hint_pk}" type="button" value="Vote">
${answer}: ${hint_text}
</p>
% endfor
% endfor
<p>
Don't like any of the hints above? Please write your own, for one of your previous incorrect answers below:
</p>
% else:
<p>
<em> Optional. </em> Help us write hints for this problem! Select one of your incorrect answers below:
</p>
% endif
<div id="answer-tabs"> <div id="answer-tabs">
<ul> <ul>
...@@ -40,36 +72,22 @@ ...@@ -40,36 +72,22 @@
</ul> </ul>
% for answer, pk_dict in answer_to_hints.items(): % for answer, pk_dict in answer_to_hints.items():
<% <%
import json import json
all_pks = json.dumps(pk_dict.keys()) all_pks = json.dumps(pk_dict.keys())
%> %>
<div class = "previous-answer" id="previous-answer-${unspace(answer)}" data-answer="${answer}" data-all-pks='${all_pks}'> <div class = "previous-answer" id="previous-answer-${unspace(answer)}" data-answer="${answer}" data-all-pks='${all_pks}'>
<div class = "hint-inner-container"> <div class = "hint-inner-container">
% if len(pk_dict) > 0:
<p>
Which hint would be most effective to show a student who also got ${answer}?
</p>
% for hint_pk, hint_text in pk_dict.items():
<p>
<input class="vote" data-answer="${answer}" data-hintno="${hint_pk}" type="button" value="Vote">
${hint_text}
</p>
% endfor
<p> <p>
Don't like any of the hints above? You can also submit your own. What hint would you give a student who made the same mistake you did? Please don't give away the answer.
</p> </p>
% endif <textarea cols="50" class="custom-hint" data-answer="${answer}">
<p>
What hint would you give a student who made the same mistake you did? Please don't give away the answer.
</p>
<textarea cols="50" class="custom-hint" data-answer="${answer}">
What would you say to help someone who got this wrong answer? What would you say to help someone who got this wrong answer?
(Don't give away the answer, please.) (Don't give away the answer, please.)
</textarea> </textarea>
<br/><br/> <br/><br/>
<input class="submit-hint" data-answer="${answer}" type="button" value="submit"> <input class="submit-hint" data-answer="${answer}" type="button" value="submit">
</div></div> </div></div>
% endfor % endfor
</div> </div>
......
...@@ -16,8 +16,8 @@ from mitxmako.shortcuts import render_to_response, render_to_string ...@@ -16,8 +16,8 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from courseware.models import XModuleContentField from courseware.models import XModuleContentField
from courseware.module_render import get_module import courseware.module_render as module_render
from courseware.model_data import ModelDataCache import courseware.model_data as model_data
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -232,8 +232,8 @@ def add_hint(request, course_id, field): ...@@ -232,8 +232,8 @@ def add_hint(request, course_id, field):
# any alternative :( # any alternative :(
loc = Location(problem_id) loc = Location(problem_id)
descriptors = modulestore().get_items(loc) descriptors = modulestore().get_items(loc)
m_d_c = ModelDataCache(descriptors, course_id, request.user) m_d_c = model_data.ModelDataCache(descriptors, course_id, request.user)
hinter_module = get_module(request.user, request, loc, m_d_c, course_id) hinter_module = module_render.get_module(request.user, request, loc, m_d_c, course_id)
signature = hinter_module.answer_signature(answer) signature = hinter_module.answer_signature(answer)
if signature is None: if signature is None:
# Signature generation failed. # Signature generation failed.
......
...@@ -2,6 +2,7 @@ import json ...@@ -2,6 +2,7 @@ import json
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from mock import MagicMock
from courseware.models import XModuleContentField from courseware.models import XModuleContentField
from courseware.tests.factories import ContentFactory from courseware.tests.factories import ContentFactory
...@@ -89,7 +90,7 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -89,7 +90,7 @@ class HintManagerTest(ModuleStoreTestCase):
out = view.get_hints(post, self.course_id, 'mod_queue') out = view.get_hints(post, self.course_id, 'mod_queue')
print out print out
self.assertTrue(out['other_field'] == 'hints') self.assertTrue(out['other_field'] == 'hints')
expected = {self.problem_id: [(u'2.0', {u'2': [u'Hint 2', 1]})]} expected = {self.problem_id: [[u'2.0', u'2.0', {u'2': [u'Hint 2', 1]}]]}
self.assertTrue(out['all_hints'] == expected) self.assertTrue(out['all_hints'] == expected)
def test_gethints_other(self): def test_gethints_other(self):
...@@ -101,9 +102,9 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -101,9 +102,9 @@ class HintManagerTest(ModuleStoreTestCase):
out = view.get_hints(post, self.course_id, 'hints') out = view.get_hints(post, self.course_id, 'hints')
print out print out
self.assertTrue(out['other_field'] == 'mod_queue') self.assertTrue(out['other_field'] == 'mod_queue')
expected = {self.problem_id: [('1.0', {'1': ['Hint 1', 2], expected = {self.problem_id: [['1.0', '1.0', {'1': ['Hint 1', 2],
'3': ['Hint 3', 12]}), '3': ['Hint 3', 12]}],
('2.0', {'4': ['Hint 4', 3]}) ['2.0', '2.0', {'4': ['Hint 4', 3]}]
]} ]}
self.assertTrue(out['all_hints'] == expected) self.assertTrue(out['all_hints'] == expected)
...@@ -137,16 +138,30 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -137,16 +138,30 @@ class HintManagerTest(ModuleStoreTestCase):
""" """
Check that instructors can add new hints. Check that instructors can add new hints.
""" """
# Because add_hint accesses the xmodule, this test requires a bunch
# of monkey patching.
import courseware.module_render as module_render
import courseware.model_data as model_data
hinter = MagicMock()
hinter.answer_signature = lambda string: string
module_render.get_module = MagicMock(return_value=hinter)
model_data.ModelDataCache = MagicMock(return_value=None)
request = RequestFactory() request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue', post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint', 'op': 'add hint',
'problem': self.problem_id, 'problem': self.problem_id,
'answer': '3.14', 'answer': '3.14',
'hint': 'This is a new hint.'}) 'hint': 'This is a new hint.'})
post.user = 'fake user'
view.add_hint(post, self.course_id, 'mod_queue') view.add_hint(post, self.course_id, 'mod_queue')
problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value
self.assertTrue('3.14' in json.loads(problem_hints)) self.assertTrue('3.14' in json.loads(problem_hints))
# Reload the things we monkey-patched.
reload(module_render)
reload(model_data)
def test_approve(self): def test_approve(self):
""" """
Check that instructors can approve hints. (Move them Check that instructors can approve hints. (Move them
......
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