Commit b64fe5c5 by Felix Sun

Finished prototype of hint moderation view. Began re-writing tests of the…

Finished prototype of hint moderation view.  Began re-writing tests of the crowdsource hinter module.  (Old tests no longer cover all the code, now that moderation has been added.)
parent 100f6bf1
from mock import Mock, patch
import unittest
import xmodule
from xmodule.crowdsource_hinter import CrowdsourceHinterModule
from xmodule.modulestore import Location
from django.http import QueryDict
from . import test_system
import json
class CHModuleFactory(object):
'''
Helps us make a CrowdsourceHinterModule with the specified internal
state.
'''
sample_problem_xml = '''
<?xml version="1.0"?>
<crowdsource_hinter>
<problem display_name="Numerical Input" markdown="A numerical input problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.&#10;&#10;The answer is correct if it is within a specified numerical tolerance of the expected answer.&#10;&#10;Enter the number of fingers on a human hand:&#10;= 5&#10;&#10;[explanation]&#10;If you look at your hand, you can count that you have five fingers. [explanation] " rerandomize="never" showanswer="finished">
<p>A numerical input problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.</p>
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
<p>Enter the number of fingers on a human hand:</p>
<numericalresponse answer="5">
<textline/>
</numericalresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>If you look at your hand, you can count that you have five fingers. </p>
</div>
</solution>
</problem>
</crowdsource_hinter>
'''
num = 0
@staticmethod
def next_num():
CHModuleFactory.num += 1
return CHModuleFactory.num
@staticmethod
def create(hints=None,
previous_answers=None,
user_voted=None,
moderate=None,
mod_queue=None):
location = Location(["i4x", "edX", "capa_test", "problem",
"SampleProblem{0}".format(CHModuleFactory.next_num())])
model_data = {'data': CHModuleFactory.sample_problem_xml}
if hints != None:
model_data['hints'] = hints
else:
model_data['hints'] = {
'24.0': {'0': ['Best hint', 40],
'3': ['Another hint', 30],
'4': ['A third hint', 20],
'6': ['A less popular hint', 3]},
'25.0': {'1': ['Really popular hint', 100]}
}
if mod_queue != None:
model_data['mod_queue'] = mod_queue
else:
model_data['mod_queue'] = {
'24.0': {'2': ['A non-approved hint']},
'26.0': {'5': ['Another non-approved hint']}
}
if previous_answers != None:
model_data['previous_answers'] = previous_answers
else:
model_data['previous_answers'] = [
['24.0', [0, 3, 4]],
['29.0', [None, None, None]]
]
if user_voted != None:
model_data['user_voted'] = user_voted
if moderate != None:
model_data['moderate'] = moderate
descriptor = Mock(weight="1")
system = test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
module = CrowdsourceHinterModule(system, descriptor, model_data)
return module
class CrowdsourceHinterTest(unittest.TestCase):
'''
In the below tests, '24.0' represents a wrong answer, and '42.5' represents
a correct answer.
'''
def test_gethint_0hint(self):
'''
Someone asks for a hint, when there's no hint to give.
- Output should be blank.
- New entry should be added to previous_answers
'''
m = CHModuleFactory.create()
json_in = {'problem_name': '26.0'}
json_out = json.loads(m.get_hint(json_in))['contents']
self.assertTrue(json_out == ' ')
self.assertTrue(['26.0', [None, None, None]] in m.previous_answers)
def test_gethint_1hint(self):
'''
Someone asks for a hint, with exactly one hint in the database.
Output should contain that hint.
'''
m = CHModuleFactory.create()
json_in = {'problem_name': '25.0'}
json_out = json.loads(m.get_hint(json_in))['contents']
self.assertTrue('Really popular hint' in json_out)
def test_gethint_manyhints(self):
'''
Someone asks for a hint, with many matching hints in the database.
- The top-rated hint should be returned.
- Two other random hints should be returned.
Currently, the best hint could be returned twice - need to fix this
in implementation.
'''
m = CHModuleFactory.create()
json_in = {'problem_name': '24.0'}
json_out = json.loads(m.get_hint(json_in))['contents']
self.assertTrue('Best hint' in json_out)
self.assertTrue(json_out.count('hint') == 3)
def test_getfeedback_0wronganswers(self):
'''
Someone has gotten the problem correct on the first try.
Output should be empty.
'''
m = CHModuleFactory.create(previous_answers=[])
json_in = {'problem_name': '42.5'}
json_out = json.loads(m.get_feedback(json_in))['contents']
self.assertTrue(json_out == ' ')
def test_getfeedback_1wronganswer_nohints(self):
'''
Someone has gotten the problem correct, with one previous wrong
answer. However, we don't actually have hints for this problem.
There should be a dialog to submit a new hint.
'''
m = CHModuleFactory.create(previous_answers=[['26.0',[None, None, None]]])
json_in = {'problem_name': '42.5'}
json_out = json.loads(m.get_feedback(json_in))['contents']
self.assertTrue('textarea' in json_out)
self.assertTrue('Vote' not in json_out)
def test_getfeedback_1wronganswer_withhints(self):
'''
Same as above, except the user did see hints. There should be
a voting dialog, with the correct choices, plus a hint submission
dialog.
'''
m = CHModuleFactory.create(hints={
'24.0': {'0': ['a hint', 42],
'1': ['another hint', 35],
'2': ['irrelevent hint', 25.0]}
},
previous_answers=[
['24.0', [0, 1, None]]],
)
json_in = {'problem_name': '42.5'}
json_out = json.loads(m.get_feedback(json_in))['contents']
self.assertTrue('a hint' in json_out)
self.assertTrue('another hint' in json_out)
self.assertTrue('irrelevent hint' not in json_out)
self.assertTrue('textarea' in json_out)
def test_vote_nopermission(self):
'''
A user tries to vote for a hint, but he has already voted!
Should not change any vote tallies.
'''
m = CHModuleFactory.create(hints={
'24.0': {'0': ['a hint', 42],
'1': ['another hint', 35],
'2': ['irrelevent hint', 25.0]}
},
previous_answers=[
['24.0', [0, 1, None]]],
user_voted=True
)
json_in = {'answer': 0, 'hint': 1}
json_out = json.loads(m.tally_vote(json_in))['contents']
self.assertTrue(m.hints['24.0']['0'][1] == 42)
self.assertTrue(m.hints['24.0']['1'][1] == 35)
self.assertTrue(m.hints['24.0']['2'][1] == 25.0)
def test_vote_withpermission(self):
'''
A user votes for a hint.
'''
m = CHModuleFactory.create(hints={
'24.0': {'0': ['a hint', 42],
'1': ['another hint', 35],
'2': ['irrelevent hint', 25.0]}
},
previous_answers=[
['24.0', [0, 1, None]]],
)
json_in = {'answer': 0, 'hint': 1}
json_out = json.loads(m.tally_vote(json_in))['contents']
self.assertTrue(m.hints['24.0']['0'][1] == 42)
self.assertTrue(m.hints['24.0']['1'][1] == 36)
self.assertTrue(m.hints['24.0']['2'][1] == 25.0)
def test_submithint_nopermission(self):
'''
A user tries to submit a hint, but he has already voted.
'''
m = CHModuleFactory.create(previous_answers=[
['24.0', [None, None, None]]],
user_voted=True)
json_in = {'answer': 0, 'hint': 'This is a new hint.'}
m.submit_hint(json_in)
self.assertTrue('24.0' not in m.hints)
def test_submithint_withpermission_new(self):
'''
A user submits a hint to an answer for which no hints
exist yet.
'''
m = CHModuleFactory.create(previous_answers=[
['24.0', [None, None, None]]],
)
json_in = {'answer': 0, 'hint': 'This is a new hint.'}
m.submit_hint(json_in)
# Make a hint request.
json_in = {'problem name': '24.0'}
json_out = json.loads(m.get_hint(json_in))['contents']
self.assertTrue('This is a new hint.' in json_out)
def test_submithint_withpermission_existing(self):
'''
A user submits a hint to an answer that has other hints
already.
'''
m = CHModuleFactory.create(previous_answers=[
['24.0', [0, None, None]]],
hints={'24.0': {'0': ['Existing hint.', 1]}}
)
json_in = {'answer': 0, 'hint': 'This is a new hint.'}
m.submit_hint(json_in)
# Make a hint request.
json_in = {'problem name': '24.0'}
json_out = json.loads(m.get_hint(json_in))['contents']
self.assertTrue('This is a new hint.' in json_out)
def test_deletehint(self):
'''
An admin / instructor deletes a hint.
'''
m = CHModuleFactory.create(hints={
'24.0': {'0': ['Deleted hint', 5],
'1': ['Safe hint', 4]}
})
m.delete_hint('24.0', '0')
json_in = {'problem name': '24.0'}
json_out = json.loads(m.get_hint(json_in))['contents']
self.assertTrue('Deleted hint' not in json_out)
self.assertTrue('Safe hint' in json_out)
......@@ -26,6 +26,8 @@ from django.core.urlresolvers import reverse
from courseware.courses import get_course_with_access
from courseware.models import XModuleContentField
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
@ensure_csrf_cookie
......@@ -48,6 +50,10 @@ def hint_manager(request, course_id):
pass
if request.POST['op'] == 'change votes':
change_votes(request, course_id, field)
if request.POST['op'] == 'add hint':
add_hint(request, course_id, field)
if request.POST['op'] == 'approve':
approve(request, course_id, field)
rendered_html = render_to_string('courseware/hint_manager_inner.html', get_hints(request, course_id, field))
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
......@@ -59,7 +65,6 @@ def get_hints(request, course_id, field):
# DON'T TRUST field attributes that come from ajax. Use an if statement
# to make sure the field is valid before plugging into functions.
out = ''
if field == 'mod_queue':
other_field = 'hints'
field_label = 'Hints Awaiting Moderation'
......@@ -71,32 +76,40 @@ def get_hints(request, course_id, field):
chopped_id = '/'.join(course_id.split('/')[:-1])
chopped_id = re.escape(chopped_id)
all_hints = XModuleContentField.objects.filter(field_name=field, definition_id__regex=chopped_id)
big_out_dict = {}
name_dict = {}
for problem in all_hints:
out += '<h2> Problem: ' + problem.definition_id + '</h2>'
for answer, hint_dict in json.loads(problem.value).items():
out += '<h4> Answer: ' + answer + '</h4>'
for pk, hint in hint_dict.items():
out += '<p data-problem="'\
+ problem.definition_id + '" data-pk="' + str(pk) + '" data-answer="'\
+ answer + '">'
out += '<input class="hint-select" type="checkbox"/>' + hint[0] + \
'<br /> Votes: <input type="text" class="votes" value="' + str(hint[1]) + '"></input>'
out += '</p>'
out += '''<h4> Add a hint to this problem </h4>
Answer (exact formatting):
<input type="text" id="new-hint-answer-''' + problem.definition_id \
+ '"/> <br /> Hint: <br /><textarea cols="50" style="height:200px" id="new-hint-' + problem.definition_id \
+ '"></textarea> <br /> <button class="submit-new-hint" data-problem="' + problem.definition_id \
+ '"> Submit </button><br />'
out += '<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>'
render_dict = {'out': out,
'field': field,
loc = Location(problem.definition_id)
try:
descriptor = modulestore().get_items(loc)[0]
except IndexError:
# Sometimes, the problem is no longer in the course. Just
# don't include said problem.
continue
name_dict[problem.definition_id] = descriptor.get_children()[0].display_name
# Answer list contains (answer, dict_of_hints) tuples.
def answer_sorter(thing):
'''
thing is a tuple, where thing[0] contains an answer, and thing[1] contains
a dict of hints. This function returns an index based on thing[0], which
is used as a key to sort the list of things.
'''
try:
return float(thing[0])
except ValueError:
# Put all non-numerical answers first.
return float('-inf')
answer_list = sorted(json.loads(problem.value).items(), key=answer_sorter)
big_out_dict[problem.definition_id] = answer_list
render_dict = {'field': field,
'other_field': other_field,
'field_label': field_label,
'other_field_label': other_field_label,
'all_hints': all_hints}
'all_hints': big_out_dict,
'id_to_name': name_dict}
return render_dict
def delete_hints(request, course_id, field):
......@@ -132,6 +145,80 @@ def change_votes(request, course_id, field):
this_problem.value = json.dumps(problem_dict)
this_problem.save()
def add_hint(request, course_id, field):
'''
Add a new hint. POST:
op
field
problem - The problem id
answer - The answer to which a hint will be added
hint - The text of the hint
'''
problem_id = request.POST['problem']
answer = request.POST['answer']
hint_text = request.POST['hint']
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)
this_pk = int(hint_pk_entry.value)
hint_pk_entry.value = this_pk + 1
hint_pk_entry.save()
problem_dict = json.loads(this_problem.value)
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()
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, answer, pk]
'''
for key in request.POST:
if key == 'op' or key == 'field':
continue
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[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 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()
......
......@@ -28,6 +28,7 @@
data_dict[i] = [$(this).parent().attr("data-problem"),
$(this).parent().attr("data-answer"),
$(this).parent().attr("data-pk")];
i += 1
}
});
$.ajax(window.location.pathname, {
......@@ -64,6 +65,40 @@
});
});
$(".submit-new-hint").click(function(){
problem_name = $(this).data("problem");
hint_text = $(".submit-hint-text").filter('*[data-problem="'+problem_name+'"]').val();
hint_answer = $(".submit-hint-answer").filter('*[data-problem="'+problem_name+'"]').val();
data_dict = {'op': 'add hint',
'field': field,
'problem': problem_name,
'answer': hint_answer,
'hint': hint_text};
$.ajax(window.location.pathname, {
type: "POST",
data: data_dict,
success: update_contents
});
});
$("#approve").click(function(){
var data_dict = {'op': 'approve',
'field': field}
var i = 1
$(".hint-select").each(function(){
if ($(this).is(":checked")) {
data_dict[i] = [$(this).parent().attr("data-problem"),
$(this).parent().attr("data-answer"),
$(this).parent().attr("data-pk")];
i += 1
}
});
$.ajax(window.location.pathname, {
type: "POST",
data: data_dict,
success: update_contents
});
});
}
$(document).ready(setup);
......
<%block name="main">
<div id="field-label" style="display:none"> ${field} </div>
<div id="field-label" style="display:none">${field}</div>
<h1> ${field_label} </h1>
Switch to <a id="switch-fields" other-field="${other_field}"> ${other_field_label} </a>
Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label}</a>
% for problem in all_hints:
<h2> Problem: ${problem.definition_id} </h2>
<%
import json
loaded_json = json.loads(problem.value).items()
%>
% for answer, hint_dict in loaded_json:
<h4> Answer: ${answer} </h4>
% for definition_id in all_hints:
<h2> Problem: ${id_to_name[definition_id]} </h2>
% 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="${problem.definition_id}" data-pk="${pk}" data-answer="${answer}">
<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])}"></input>
Votes: <input type="text" class="votes" value="${str(hint[1])}" style="font-size:12px; height:20px; width:50px"></input>
<br /><br />
</p>
% endfor
% if len(hint_dict) > 0:
</div><br />
% endif
% endfor
<h4> Add a hint to this problem </h4>
Answer (exact formatting):
<input type="text" id="new-hint-answer-${problem.definition_id}"/>
<h4> Answer: </h4>
<input type="text" class="submit-hint-answer" data-problem="${definition_id}"/>
(Be sure to format your answer in the same way as the other answers you see here.)
<br />
Hint: <br />
<textarea cols="50" style="height:200px" id="new-hint-${problem.definition_id}"></textarea>
<textarea cols="50" style="height:200px" class="submit-hint-text" data-problem="${definition_id}"></textarea>
<br />
<button class="submit-new-hint" data-problem="${problem.definition_id}"> Submit </button>
<button class="submit-new-hint" data-problem="${definition_id}"> Submit </button>
<br />
% endfor
<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>
% if field == 'mod_queue':
<button id="approve"> Approve selected </button>
% endif
</%block>
\ No newline at end of file
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