")
+ 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)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py
index 431d3f5d7c..5d722b1e79 100644
--- a/lms/djangoapps/instructor/hint_manager.py
+++ b/lms/djangoapps/instructor/hint_manager.py
@@ -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 += '
Problem: ' + problem.definition_id + '
'
- for answer, hint_dict in json.loads(problem.value).items():
- out += '
Answer: ' + answer + '
'
- for pk, hint in hint_dict.items():
- out += '
'
- out += '' + hint[0] + \
- ' Votes: '
- out += '
'
- out += '''
Add a hint to this problem
- Answer (exact formatting):
- Hint:
'
+ 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')
- out += ' '
- render_dict = {'out': out,
- 'field': field,
+ 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):
@@ -131,6 +144,80 @@ def change_votes(request, course_id, field):
problem_dict[answer][pk][1] = new_votes
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()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lms/templates/courseware/hint_manager.html b/lms/templates/courseware/hint_manager.html
index 94156d3d68..394792f892 100644
--- a/lms/templates/courseware/hint_manager.html
+++ b/lms/templates/courseware/hint_manager.html
@@ -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);
diff --git a/lms/templates/courseware/hint_manager_inner.html b/lms/templates/courseware/hint_manager_inner.html
index 41e8d018c5..c69539522f 100644
--- a/lms/templates/courseware/hint_manager_inner.html
+++ b/lms/templates/courseware/hint_manager_inner.html
@@ -1,39 +1,45 @@
<%block name="main">
-
- <%
- import json
- loaded_json = json.loads(problem.value).items()
- %>
- % for answer, hint_dict in loaded_json:
-
Answer: ${answer}
+% for definition_id in all_hints:
+
Problem: ${id_to_name[definition_id]}
+ % for answer, hint_dict in all_hints[definition_id]:
+ % if len(hint_dict) > 0:
+
Answer: ${answer}
+ % endif
% for pk, hint in hint_dict.items():
-
+
${hint[0]}
- Votes:
+ Votes:
+
% endfor
+ % if len(hint_dict) > 0:
+
+ % endif
% endfor
Add a hint to this problem
- Answer (exact formatting):
-
+
Answer:
+
+ (Be sure to format your answer in the same way as the other answers you see here.)
Hint:
-
+
-
+
% endfor
-
+
+% if field == 'mod_queue':
+
+% endif
%block>
\ No newline at end of file