Fixed tests for removing hash access to hints. Fixed instructor view for same.
This commit is contained in:
@@ -188,6 +188,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
if not self.validate_answer(answer):
|
||||
# Answer is not in the right form.
|
||||
log.exception('Answer not valid: ' + str(answer))
|
||||
return
|
||||
if answer not in self.user_submissions:
|
||||
self.user_submissions += [answer]
|
||||
# Look for a hint to give.
|
||||
@@ -219,12 +220,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
self.previous_answers += [[best_hint_answer, [best_hint_index]]]
|
||||
for i in xrange(min(2, n_hints-1)):
|
||||
# Keep making random hints until we hit a target, or run out.
|
||||
(hint_index, (rand_hint, votes, hint_answer)) =\
|
||||
random.choice(matching_hints.items())
|
||||
if rand_hint in hints:
|
||||
# Don't show the same hint twice. Try again.
|
||||
i -= 1
|
||||
continue
|
||||
go_on = False
|
||||
while not go_on:
|
||||
(hint_index, (rand_hint, votes, hint_answer)) =\
|
||||
random.choice(matching_hints.items())
|
||||
if not rand_hint in hints:
|
||||
go_on = True
|
||||
hints.append(rand_hint)
|
||||
self.previous_answers += [[hint_index, [hint_answer]]]
|
||||
return {'hints': hints,
|
||||
|
||||
@@ -55,6 +55,7 @@ class CHModuleFactory(object):
|
||||
@staticmethod
|
||||
def create(hints=None,
|
||||
previous_answers=None,
|
||||
user_submissions=None,
|
||||
user_voted=None,
|
||||
moderate=None,
|
||||
mod_queue=None):
|
||||
@@ -87,9 +88,14 @@ class CHModuleFactory(object):
|
||||
else:
|
||||
model_data['previous_answers'] = [
|
||||
['24.0', [0, 3, 4]],
|
||||
['29.0', [None, None, None]]
|
||||
['29.0', []]
|
||||
]
|
||||
|
||||
if user_submissions is not None:
|
||||
model_data['user_submissions'] = user_submissions
|
||||
else:
|
||||
model_data['user_submissions'] = ['24.0', '29.0']
|
||||
|
||||
if user_voted is not None:
|
||||
model_data['user_voted'] = user_voted
|
||||
|
||||
@@ -104,7 +110,24 @@ class CHModuleFactory(object):
|
||||
|
||||
# Make a fake capa module.
|
||||
capa_module = MagicMock()
|
||||
capa_module.responders = {'responder0': MagicMock()}
|
||||
capa_module.lcp = MagicMock()
|
||||
responder = MagicMock()
|
||||
|
||||
def validate_answer(answer):
|
||||
""" A mock answer validator - simulates a numerical response"""
|
||||
try:
|
||||
float(answer)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
responder.validate_answer = validate_answer
|
||||
|
||||
def answer_compare(a, b):
|
||||
""" A fake answer comparer """
|
||||
return a == b
|
||||
responder.answer_compare = answer_compare
|
||||
|
||||
capa_module.lcp.responders = {'responder0': responder}
|
||||
capa_module.displayable_items = lambda: [capa_module]
|
||||
|
||||
system = get_test_system()
|
||||
@@ -122,28 +145,6 @@ class CHModuleFactory(object):
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -288,46 +289,6 @@ class CrowdsourceHinterTest(unittest.TestCase):
|
||||
parsed = mock_module.formula_answer_to_str(get)
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Someone asks for a hint, when there's no hint to give.
|
||||
@@ -337,8 +298,9 @@ class CrowdsourceHinterTest(unittest.TestCase):
|
||||
mock_module = CHModuleFactory.create()
|
||||
json_in = {'problem_name': '26.0'}
|
||||
out = mock_module.get_hint(json_in)
|
||||
print mock_module.previous_answers
|
||||
self.assertTrue(out is None)
|
||||
self.assertTrue(['26.0', [None, None, None]] in mock_module.previous_answers)
|
||||
self.assertTrue('26.0' in mock_module.user_submissions)
|
||||
|
||||
def test_gethint_unparsable(self):
|
||||
"""
|
||||
@@ -359,10 +321,13 @@ class CrowdsourceHinterTest(unittest.TestCase):
|
||||
"""
|
||||
mock_module = CHModuleFactory.create()
|
||||
old_answers = copy.deepcopy(mock_module.previous_answers)
|
||||
old_user_submissions = copy.deepcopy(mock_module.user_submissions)
|
||||
json_in = {'problem1': 'fish'}
|
||||
out = mock_module.get_hint(json_in)
|
||||
self.assertTrue(out is None)
|
||||
self.assertTrue(mock_module.previous_answers == old_answers)
|
||||
self.assertTrue(mock_module.user_submissions == old_user_submissions)
|
||||
|
||||
|
||||
def test_gethint_1hint(self):
|
||||
"""
|
||||
@@ -372,7 +337,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
|
||||
mock_module = CHModuleFactory.create()
|
||||
json_in = {'problem_name': '25.0'}
|
||||
out = mock_module.get_hint(json_in)
|
||||
self.assertTrue(out['best_hint'] == 'Really popular hint')
|
||||
self.assertTrue('Really popular hint' in out['hints'])
|
||||
# Also make sure that the input gets added to user_submissions,
|
||||
# and that the hint is logged in previous_answers.
|
||||
self.assertTrue('25.0' in mock_module.user_submissions)
|
||||
self.assertTrue(['25.0', ['1']] in mock_module.previous_answers)
|
||||
|
||||
def test_gethint_manyhints(self):
|
||||
"""
|
||||
@@ -385,9 +354,8 @@ class CrowdsourceHinterTest(unittest.TestCase):
|
||||
mock_module = CHModuleFactory.create()
|
||||
json_in = {'problem_name': '24.0'}
|
||||
out = mock_module.get_hint(json_in)
|
||||
self.assertTrue(out['best_hint'] == 'Best hint')
|
||||
self.assertTrue('rand_hint_1' in out)
|
||||
self.assertTrue('rand_hint_2' in out)
|
||||
self.assertTrue('Best hint' in out['hints'])
|
||||
self.assertTrue(len(out['hints']) == 3)
|
||||
|
||||
def test_getfeedback_0wronganswers(self):
|
||||
"""
|
||||
@@ -527,8 +495,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
|
||||
# Make a hint request.
|
||||
json_in = {'problem name': '25.0'}
|
||||
out = mock_module.get_hint(json_in)
|
||||
self.assertTrue((out['best_hint'] == 'This is a new hint.')
|
||||
or (out['rand_hint_1'] == 'This is a new hint.'))
|
||||
self.assertTrue('This is a new hint.' in out['hints'])
|
||||
|
||||
def test_submithint_moderate(self):
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,6 @@ import re
|
||||
|
||||
from django.http import HttpResponse, Http404
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
@@ -114,27 +113,8 @@ def get_hints(request, course_id, field):
|
||||
# Put all non-numerical answers first.
|
||||
return float('-inf')
|
||||
|
||||
# Find the signature to answer converter for this problem. Sometimes,
|
||||
# it doesn't exist; just assume that the signatures are the answers.
|
||||
try:
|
||||
signature_to_ans = XModuleContentField.objects.get(
|
||||
field_name='signature_to_ans',
|
||||
definition_id__regex=chopped_id
|
||||
)
|
||||
signature_to_ans = json.loads(signature_to_ans.value)
|
||||
except ObjectDoesNotExist:
|
||||
signature_to_ans = {}
|
||||
|
||||
signatures_dict = json.loads(hints_by_problem.value)
|
||||
unsorted = []
|
||||
for signature, dict_of_hints in signatures_dict.items():
|
||||
if signature in signature_to_ans:
|
||||
ans_txt = signature_to_ans[signature]
|
||||
else:
|
||||
ans_txt = signature
|
||||
unsorted.append([signature, ans_txt, dict_of_hints])
|
||||
# Answer list contains [signature, answer, dict_of_hints] sub-lists.
|
||||
answer_list = sorted(unsorted, key=answer_sorter)
|
||||
# Answer list contains [answer, dict_of_hints] pairs.
|
||||
answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter)
|
||||
big_out_dict[hints_by_problem.definition_id] = answer_list
|
||||
|
||||
render_dict = {'field': field,
|
||||
@@ -165,7 +145,7 @@ def delete_hints(request, course_id, field):
|
||||
Deletes the hints specified.
|
||||
|
||||
`request.POST` contains some fields keyed by integers. Each such field contains a
|
||||
[problem_defn_id, signature, pk] tuple. These tuples specify the hints to be deleted.
|
||||
[problem_defn_id, answer, pk] tuple. These tuples specify the hints to be deleted.
|
||||
|
||||
Example `request.POST`:
|
||||
{'op': 'delete_hints',
|
||||
@@ -177,12 +157,12 @@ def delete_hints(request, course_id, field):
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, signature, pk = request.POST.getlist(key)
|
||||
problem_id, answer, pk = request.POST.getlist(key)
|
||||
# Can be optimized - sort the delete list by problem_id, and load each problem
|
||||
# from the database only once.
|
||||
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
del problem_dict[signature][pk]
|
||||
del problem_dict[answer][pk]
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
@@ -191,18 +171,18 @@ def change_votes(request, course_id, field):
|
||||
"""
|
||||
Updates the number of votes.
|
||||
|
||||
The numbered fields of `request.POST` contain [problem_id, signature, pk, new_votes] tuples.
|
||||
The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples.
|
||||
- Very similar to `delete_hints`. Is there a way to merge them? Nah, too complicated.
|
||||
"""
|
||||
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, signature, pk, new_votes = request.POST.getlist(key)
|
||||
problem_id, answer, pk, new_votes = request.POST.getlist(key)
|
||||
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
# problem_dict[signature][pk] points to a [hint_text, #votes] pair.
|
||||
problem_dict[signature][pk][1] = int(new_votes)
|
||||
# problem_dict[answer][pk] points to a [hint_text, #votes] pair.
|
||||
problem_dict[answer][pk][1] = int(new_votes)
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
@@ -214,13 +194,24 @@ def add_hint(request, course_id, field):
|
||||
field
|
||||
problem - The problem id
|
||||
answer - The answer to which a hint will be added
|
||||
- Needs to be converted into signature first.
|
||||
hint - The text of the hint
|
||||
"""
|
||||
|
||||
problem_id = request.POST['problem']
|
||||
answer = request.POST['answer']
|
||||
hint_text = request.POST['hint']
|
||||
|
||||
# Validate the answer. This requires initializing the xmodules, which
|
||||
# is annoying.
|
||||
loc = Location(problem_id)
|
||||
descriptors = modulestore().get_items(loc)
|
||||
m_d_c = model_data.ModelDataCache(descriptors, course_id, request.user)
|
||||
hinter_module = module_render.get_module(request.user, request, loc, m_d_c, course_id)
|
||||
if not hinter_module.validate_answer(answer):
|
||||
# Invalid answer. Don't add it to the database, or else the
|
||||
# hinter will crash when we encounter it.
|
||||
return 'Error - the answer you specified is not properly formatted: ' + str(answer)
|
||||
|
||||
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
|
||||
hint_pk_entry = XModuleContentField.objects.get(field_name='hint_pk', definition_id=problem_id)
|
||||
@@ -228,23 +219,10 @@ def add_hint(request, course_id, field):
|
||||
hint_pk_entry.value = this_pk + 1
|
||||
hint_pk_entry.save()
|
||||
|
||||
# Make signature. This is really annoying, but I don't see
|
||||
# any alternative :(
|
||||
loc = Location(problem_id)
|
||||
descriptors = modulestore().get_items(loc)
|
||||
m_d_c = model_data.ModelDataCache(descriptors, course_id, request.user)
|
||||
hinter_module = module_render.get_module(request.user, request, loc, m_d_c, course_id)
|
||||
signature = hinter_module.answer_signature(answer)
|
||||
if signature is None:
|
||||
# Signature generation failed.
|
||||
# We should probably return an error message, too... working on that.
|
||||
return 'Error - your answer could not be parsed as a formula expression.'
|
||||
hinter_module.add_signature(signature, answer)
|
||||
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
if signature not in problem_dict:
|
||||
problem_dict[signature] = {}
|
||||
problem_dict[signature][this_pk] = [hint_text, 1]
|
||||
if answer not in problem_dict:
|
||||
problem_dict[answer] = {}
|
||||
problem_dict[answer][this_pk] = [hint_text, 1]
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
@@ -254,26 +232,26 @@ def approve(request, course_id, field):
|
||||
Approve a list of hints, moving them from the mod_queue to the real
|
||||
hint list. POST:
|
||||
op, field
|
||||
(some number) -> [problem, signature, pk]
|
||||
(some number) -> [problem, answer, pk]
|
||||
"""
|
||||
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, signature, pk = request.POST.getlist(key)
|
||||
problem_id, answer, pk = request.POST.getlist(key)
|
||||
# Can be optimized - sort the delete list by problem_id, and load each problem
|
||||
# from the database only once.
|
||||
problem_in_mod = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
problem_dict = json.loads(problem_in_mod.value)
|
||||
hint_to_move = problem_dict[signature][pk]
|
||||
del problem_dict[signature][pk]
|
||||
hint_to_move = problem_dict[answer][pk]
|
||||
del problem_dict[answer][pk]
|
||||
problem_in_mod.value = json.dumps(problem_dict)
|
||||
problem_in_mod.save()
|
||||
|
||||
problem_in_hints = XModuleContentField.objects.get(field_name='hints', definition_id=problem_id)
|
||||
problem_dict = json.loads(problem_in_hints.value)
|
||||
if signature not in problem_dict:
|
||||
problem_dict[signature] = {}
|
||||
problem_dict[signature][pk] = hint_to_move
|
||||
if answer not in problem_dict:
|
||||
problem_dict[answer] = {}
|
||||
problem_dict[answer][pk] = hint_to_move
|
||||
problem_in_hints.value = json.dumps(problem_dict)
|
||||
problem_in_hints.save()
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from mock import MagicMock, patch
|
||||
from mock import patch, MagicMock
|
||||
|
||||
from courseware.models import XModuleContentField
|
||||
from courseware.tests.factories import ContentFactory
|
||||
@@ -12,8 +12,6 @@ from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class HintManagerTest(ModuleStoreTestCase):
|
||||
@@ -92,7 +90,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
out = view.get_hints(post, self.course_id, 'mod_queue')
|
||||
print out
|
||||
self.assertTrue(out['other_field'] == 'hints')
|
||||
expected = {self.problem_id: [[u'2.0', u'2.0', {u'2': [u'Hint 2', 1]}]]}
|
||||
expected = {self.problem_id: [(u'2.0', {u'2': [u'Hint 2', 1]})]}
|
||||
self.assertTrue(out['all_hints'] == expected)
|
||||
|
||||
def test_gethints_other(self):
|
||||
@@ -104,9 +102,9 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
out = view.get_hints(post, self.course_id, 'hints')
|
||||
print out
|
||||
self.assertTrue(out['other_field'] == 'mod_queue')
|
||||
expected = {self.problem_id: [['1.0', '1.0', {'1': ['Hint 1', 2],
|
||||
'3': ['Hint 3', 12]}],
|
||||
['2.0', '2.0', {'4': ['Hint 4', 3]}]
|
||||
expected = {self.problem_id: [('1.0', {'1': ['Hint 1', 2],
|
||||
'3': ['Hint 3', 12]}),
|
||||
('2.0', {'4': ['Hint 4', 3]})
|
||||
]}
|
||||
self.assertTrue(out['all_hints'] == expected)
|
||||
|
||||
@@ -143,7 +141,7 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
# Because add_hint accesses the xmodule, this test requires a bunch
|
||||
# of monkey patching.
|
||||
hinter = MagicMock()
|
||||
hinter.answer_signature = lambda string: string
|
||||
hinter.validate_answer = lambda string: True
|
||||
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'mod_queue',
|
||||
@@ -158,6 +156,27 @@ class HintManagerTest(ModuleStoreTestCase):
|
||||
problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value
|
||||
self.assertTrue('3.14' in json.loads(problem_hints))
|
||||
|
||||
def test_addbadhint(self):
|
||||
"""
|
||||
Check that instructors cannot add hints with unparsable answers.
|
||||
"""
|
||||
# Patching.
|
||||
hinter = MagicMock()
|
||||
hinter.validate_answer = lambda string: False
|
||||
|
||||
request = RequestFactory()
|
||||
post = request.post(self.url, {'field': 'mod_queue',
|
||||
'op': 'add hint',
|
||||
'problem': self.problem_id,
|
||||
'answer': 'fish',
|
||||
'hint': 'This is a new hint.'})
|
||||
post.user = 'fake user'
|
||||
with patch('courseware.module_render.get_module', MagicMock(return_value=hinter)):
|
||||
with patch('courseware.model_data.ModelDataCache', MagicMock(return_value=None)):
|
||||
view.add_hint(post, self.course_id, 'mod_queue')
|
||||
problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value
|
||||
self.assertTrue('fish' not in json.loads(problem_hints))
|
||||
|
||||
def test_approve(self):
|
||||
"""
|
||||
Check that instructors can approve hints. (Move them
|
||||
|
||||
@@ -8,12 +8,12 @@ Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label
|
||||
|
||||
% for definition_id in all_hints:
|
||||
<h2> Problem: ${id_to_name[definition_id]} </h2>
|
||||
% for signature, answer, hint_dict in all_hints[definition_id]:
|
||||
% for answer, hint_dict in all_hints[definition_id]:
|
||||
% if len(hint_dict) > 0:
|
||||
<h4> Answer: ${answer} </h4><div style="background-color:#EEEEEE">
|
||||
% endif
|
||||
% for pk, hint in hint_dict.items():
|
||||
<p data-problem="${definition_id}" data-pk="${pk}" data-answer="${signature}">
|
||||
<p data-problem="${definition_id}" data-pk="${pk}" data-answer="${answer}">
|
||||
<input class="hint-select" type="checkbox"/> ${hint[0]}
|
||||
<br />
|
||||
Votes: <input type="text" class="votes" value="${str(hint[1])}" style="font-size:12px; height:20px; width:50px"></input>
|
||||
|
||||
Reference in New Issue
Block a user