diff --git a/common/lib/xmodule/xmodule/crowdsource_hinter.py b/common/lib/xmodule/xmodule/crowdsource_hinter.py index da6ae989e3..c5d5dd7f80 100644 --- a/common/lib/xmodule/xmodule/crowdsource_hinter.py +++ b/common/lib/xmodule/xmodule/crowdsource_hinter.py @@ -23,31 +23,31 @@ log = logging.getLogger(__name__) class CrowdsourceHinterFields(object): has_children = True - hints = Dict(help='''A dictionary mapping answers to lists of [hint, number_of_votes] pairs. - ''', scope=Scope.content, default= {}) + hints = Dict(help="""A dictionary mapping answers to lists of [hint, number_of_votes] pairs. + """, scope=Scope.content, default= {}) - previous_answers = List(help='''A list of previous answers this student made to this problem. + previous_answers = List(help="""A list of previous answers this student made to this problem. Of the form (answer, (hint_id_1, hint_id_2, hint_id_3)) for each problem. hint_id's are - None if the hint was not given.''', + None if the hint was not given.""", scope=Scope.user_state, default=[]) user_voted = Boolean(help='Specifies if the user has voted on this problem or not.', scope=Scope.user_state, default=False) - moderate = String(help='''If True, then all hints must be approved by staff before + moderate = String(help="""If True, then all hints must be approved by staff before becoming visible. - This field is automatically populated from the xml metadata.''', scope=Scope.content, + This field is automatically populated from the xml metadata.""", scope=Scope.content, default='False') - mod_queue = Dict(help='''Contains hints that have not been approved by the staff yet. Structured - identically to the hints dictionary.''', scope=Scope.content, default={}) + mod_queue = Dict(help="""Contains hints that have not been approved by the staff yet. Structured + identically to the hints dictionary.""", scope=Scope.content, default={}) hint_pk = Integer(help='Used to index hints.', scope=Scope.content, default=0) class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): - ''' An Xmodule that makes crowdsourced hints. - ''' + """ An Xmodule that makes crowdsourced hints. + """ icon_class = 'crowdsource_hinter' js = {'coffee': [resource_string(__name__, 'js/src/crowdsource_hinter/display.coffee'), @@ -61,10 +61,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): def get_html(self): - ''' + """ Does a regular expression find and replace to change the AJAX url. - Dependent on lon-capa problem. - ''' + """ # Reset the user vote, for debugging only! Remove for prod. self.user_voted = False # You are invited to guess what the lines below do :) @@ -82,10 +82,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): return out def capa_make_answer_hashable(self, answer): - ''' + """ Capa answer format: dict[problem name] -> [list of answers] Output format: ((problem name, (answers))) - ''' + """ out = [] for problem, a in answer.items(): out.append((problem, tuple(a))) @@ -93,18 +93,18 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): def ans_to_text(self, answer): - ''' + """ Converts capa answer format to a string representation of the answer. -Lon-capa dependent. - ''' + """ return str(float(answer.values()[0])) def handle_ajax(self, dispatch, get): - ''' + """ This is the landing method for AJAX calls. - ''' + """ if dispatch == 'get_hint': out = self.get_hint(get) if dispatch == 'get_feedback': @@ -122,33 +122,35 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): def get_hint(self, get): - ''' + """ The student got the incorrect answer found in get. Give him a hint. - ''' + """ answer = self.ans_to_text(get) # Look for a hint to give. - if (answer not in self.hints) or (len(self.hints[answer]) == 0): + # Make a local copy of self.hints - this means we only need to do one json unpacking. + local_hints = self.hints + if (answer not in local_hints) or (len(local_hints[answer]) == 0): # No hints to give. Return. self.previous_answers += [[answer, [None, None, None]]] return # Get the top hint, plus two random hints. - n_hints = len(self.hints[answer]) - best_hint_index = max(self.hints[answer], key=lambda key: self.hints[answer][key][1]) - best_hint = self.hints[answer][best_hint_index][0] - if len(self.hints[answer]) == 1: + n_hints = len(local_hints[answer]) + best_hint_index = max(local_hints[answer], key=lambda key: local_hints[answer][key][1]) + best_hint = local_hints[answer][best_hint_index][0] + if len(local_hints[answer]) == 1: rand_hint_1 = '' rand_hint_2 = '' self.previous_answers += [[answer, [best_hint_index, None, None]]] elif n_hints == 2: - best_hint = self.hints[answer].values()[0][0] - best_hint_index = self.hints[answer].keys()[0] - rand_hint_1 = self.hints[answer].values()[1][0] - hint_index_1 = self.hints[answer].keys()[1] + best_hint = local_hints[answer].values()[0][0] + best_hint_index = local_hints[answer].keys()[0] + rand_hint_1 = local_hints[answer].values()[1][0] + hint_index_1 = local_hints[answer].keys()[1] rand_hint_2 = '' self.previous_answers += [[answer, [best_hint_index, hint_index_1, None]]] else: (hint_index_1, rand_hint_1), (hint_index_2, rand_hint_2) =\ - random.sample(self.hints[answer].items(), 2) + random.sample(local_hints[answer].items(), 2) rand_hint_1 = rand_hint_1[0] rand_hint_2 = rand_hint_2[0] self.previous_answers += [(answer, (best_hint_index, hint_index_1, hint_index_2))] @@ -159,9 +161,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): 'answer': answer} def get_feedback(self, get): - ''' + """ The student got it correct. Ask him to vote on hints, or submit a hint. - ''' + """ # The student got it right. # Did he submit at least one wrong answer? out = '' @@ -181,11 +183,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): index_to_hints[i] = [] index_to_answer[i] = answer if answer in self.hints: - # Add each hint to the html string, with a vote button. for hint_id in hints_offered: if hint_id != None: try: - index_to_hints[i].append((self.hints[answer][hint_id][0], hint_id)) + index_to_hints[i].append((self.hints[answer][str(hint_id)][0], hint_id)) except KeyError: # Sometimes, the hint that a user saw will have been deleted by the instructor. continue @@ -194,12 +195,12 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): def tally_vote(self, get): - ''' + """ Tally a user's vote on his favorite hint. get: 'answer': ans_no (index in previous_answers) 'hint': hint_no - ''' + """ if self.user_voted: return json.dumps({'contents': 'Sorry, but you have already voted!'}) ans_no = int(get['answer']) @@ -211,19 +212,25 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): self.hints = temp_dict # Don't let the user vote again! self.user_voted = True + + # Return a list of how many votes each hint got. + hint_and_votes = [] + for hint_no in self.previous_answers[ans_no][1]: + if hint_no == None: + continue + hint_and_votes.append(temp_dict[answer][str(hint_no)]) + # Reset self.previous_answers. self.previous_answers = [] - # In the future, return a list of how many votes each hint got, maybe? - return {'message': 'Congrats, you\'ve voted!'} - + return {'hint_and_votes': hint_and_votes} def submit_hint(self, get): - ''' + """ Take a hint submission and add it to the database. get: 'answer': answer index in previous_answers 'hint': text of the new hint that the user is adding - ''' + """ # Do html escaping. Perhaps in the future do profanity filtering, etc. as well. hint = escape(get['hint']) answer = self.previous_answers[int(get['answer'])][0] @@ -251,11 +258,11 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule): def delete_hint(self, answer, hint_id): - ''' + """ From the answer, delete the hint with hint_id. Not designed to be accessed via POST request, for now. -LIKELY DEPRECATED. - ''' + """ temp_hints = self.hints del temp_hints[answer][str(hint_id)] self.hints = temp_hints diff --git a/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee b/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee index a44bbf44f6..8eeab4cb02 100644 --- a/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee @@ -13,7 +13,6 @@ class @Hinter # request. answers = data[0] response = data[1] - console.debug(response) if response.search(/class="correct/) == -1 # Incorrect. Get hints. $.postWithPrefix "#{@url}/get_hint", answers, (response) => @@ -29,9 +28,9 @@ class @Hinter bind: => window.update_schematics() @$('input.vote').click @vote - @$('#feedback-select').change @feedback_ui_change @$('input.submit-hint').click @submit_hint @$('.custom-hint').click @clear_default_text + @$('#answer-tabs').tabs({active: 0}) vote: (eventObj) => diff --git a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py index 1eafccda0f..b70c490644 100644 --- a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py +++ b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py @@ -1,6 +1,7 @@ from mock import Mock, patch import unittest import copy +import random import xmodule from xmodule.crowdsource_hinter import CrowdsourceHinterModule @@ -13,12 +14,12 @@ from . import get_test_system import json class CHModuleFactory(object): - ''' + """ Helps us make a CrowdsourceHinterModule with the specified internal state. - ''' + """ - sample_problem_xml = ''' + sample_problem_xml = """ @@ -36,7 +37,7 @@ class CHModuleFactory(object): - ''' + """ num = 0 @@ -91,102 +92,101 @@ class CHModuleFactory(object): descriptor = Mock(weight="1") system = get_test_system() - system.render_template = Mock(return_value="
Test Template HTML
") 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 == ' ') + out = m.get_hint(json_in) + self.assertTrue(out == None) 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) + out = m.get_hint(json_in) + self.assertTrue(out['best_hint'] == 'Really popular hint') 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'] - print json_out - self.assertTrue('Best hint' in json_out) - self.assertTrue(json_out.count('hint') == 3) + out = m.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) 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 == ' ') + out = m.get_feedback(json_in) + self.assertTrue(out == None) 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) + out = m.get_feedback(json_in) + print out['index_to_answer'] + self.assertTrue(out['index_to_hints'][0] == []) + self.assertTrue(out['index_to_answer'][0] == '26.0') 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( previous_answers=[ ['24.0', [0, 3, None]]], ) json_in = {'problem_name': '42.5'} - json_out = json.loads(m.get_feedback(json_in))['contents'] - self.assertTrue('Best hint' in json_out) - self.assertTrue('Another hint' in json_out) - self.assertTrue('third hint' not in json_out) - self.assertTrue('textarea' in json_out) - + out = m.get_feedback(json_in) + self.assertTrue(len(out['index_to_hints'][0])==2) 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(user_voted=True) json_in = {'answer': 0, 'hint': 1} old_hints = copy.deepcopy(m.hints) @@ -195,21 +195,21 @@ class CrowdsourceHinterTest(unittest.TestCase): def test_vote_withpermission(self): - ''' + """ A user votes for a hint. - ''' + """ m = CHModuleFactory.create() json_in = {'answer': 0, 'hint': 3} - json_out = json.loads(m.tally_vote(json_in))['contents'] + m.tally_vote(json_in) self.assertTrue(m.hints['24.0']['0'][1] == 40) self.assertTrue(m.hints['24.0']['3'][1] == 31) self.assertTrue(m.hints['24.0']['4'][1] == 20) def test_submithint_nopermission(self): - ''' + """ A user tries to submit a hint, but he has already voted. - ''' + """ m = CHModuleFactory.create(user_voted=True) json_in = {'answer': 1, 'hint': 'This is a new hint.'} print m.user_voted @@ -219,39 +219,37 @@ class CrowdsourceHinterTest(unittest.TestCase): def test_submithint_withpermission_new(self): - ''' + """ A user submits a hint to an answer for which no hints exist yet. - ''' + """ m = CHModuleFactory.create() json_in = {'answer': 1, 'hint': 'This is a new hint.'} m.submit_hint(json_in) - # Make a hint request. - json_in = {'problem name': '29.0'} - json_out = json.loads(m.get_hint(json_in))['contents'] - self.assertTrue('This is a new hint.' in json_out) + self.assertTrue('29.0' in m.hints) def test_submithint_withpermission_existing(self): - ''' + """ A user submits a hint to an answer that has other hints already. - ''' + """ m = CHModuleFactory.create(previous_answers = [['25.0', [1, 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': '25.0'} - json_out = json.loads(m.get_hint(json_in))['contents'] - self.assertTrue('This is a new hint.' in json_out) + out = m.get_hint(json_in) + self.assertTrue((out['best_hint'] == 'This is a new hint.') + or (out['rand_hint_1'] == 'This is a new hint.')) def test_submithint_moderate(self): - ''' + """ A user submits a hint, but moderation is on. The hint should show up in the mod_queue, not the public-facing hints dict. - ''' + """ m = CHModuleFactory.create(moderate='True') json_in = {'answer': 1, 'hint': 'This is a new hint.'} m.submit_hint(json_in) @@ -259,9 +257,28 @@ class CrowdsourceHinterTest(unittest.TestCase): self.assertTrue('29.0' in m.mod_queue) - - - + def test_template_gethint(self): + """ + Test the templates for get_hint. + """ + m = CHModuleFactory.create() + + def fake_get_hint(get): + """ + Creates a rendering dictionary, with which we can test + the templates. + """ + return {'best_hint': 'This is the best hint.', + 'rand_hint_1': 'A random hint', + 'rand_hint_2': 'Another random hint', + 'answer': '42.5'} + + m.get_hint = fake_get_hint + json_in = {'problem_name': '42.5'} + out = json.loads(m.handle_ajax('get_hint', json_in))['contents'] + self.assertTrue('This is the best hint.' in out) + self.assertTrue('A random hint' in out) + self.assertTrue('Another random hint' in out) diff --git a/common/templates/hinter_display.html b/common/templates/hinter_display.html new file mode 100644 index 0000000000..e1f9a5fafb --- /dev/null +++ b/common/templates/hinter_display.html @@ -0,0 +1,93 @@ +## The hinter module passes in a field called ${op}, which determines which +## sub-function to render. + + +<%def name="get_hint()"> + % if best_hint != '': +

Other students who arrvied at the wrong answer of ${answer} recommend the following hints:

+ + + +<%def name="get_feedback()"> + Participation in the hinting system is strictly optional, and will not influence + your grade. +
+ Help us improve our hinting system. Start by picking one of your previous incorrect answers from below: +

+ +
+ + + % for index, answer in index_to_answer.items(): +
+ % if index in index_to_hints and len(index_to_hints[index]) > 0: + Which hint was most helpful when you got the wrong answer of ${answer}? +
+ % for hint_text, hint_pk in index_to_hints[index]: + + ${hint_text} +
+ % endfor + Don't like any of the hints above? You can also submit your own. + % else: + Write a hint for other students who get the wrong answer of ${answer}. + % endif + Try to describe what concepts you misunderstood, or what mistake you made. Please don't + give away the answer. + + + +
+ % endfor +
+ + + +<%def name="show_votes()"> + Thank you for voting! +
+ % for hint, votes in hint_and_votes: + ${votes} votes. + ${hint} +
+ % endfor + + +<%def name="simple_message()"> + ${message} + + +% if op == "get_hint": + ${get_hint()} +% endif + +% if op == "get_feedback": + ${get_feedback()} +% endif + +% if op == "submit_hint": + ${simple_message()} +% endif + +% if op == "vote": + ${show_votes()} +% endif + + +