Commit c79ca38f by Felix Sun

Moved the hinter rendering onto a mako template.

Hinter now displays vote count after voting.

Began testing templates.
parent bb922ed8
......@@ -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
......
......@@ -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) =>
......
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 = """
<?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">
......@@ -36,7 +37,7 @@ class CHModuleFactory(object):
</solution>
</problem>
</crowdsource_hinter>
'''
"""
num = 0
......@@ -91,102 +92,101 @@ class CHModuleFactory(object):
descriptor = Mock(weight="1")
system = get_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 == ' ')
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)
......
## The hinter module passes in a field called ${op}, which determines which
## sub-function to render.
<%def name="get_hint()">
% if best_hint != '':
<h4> Other students who arrvied at the wrong answer of ${answer} recommend the following hints: </h4>
<ul>
<li> ${best_hint} </li>
% endif
% if rand_hint_1 != '':
<li> ${rand_hint_1} </li>
% endif
% if rand_hint_2 != '':
<li> ${rand_hint_2} </li>
% endif
</ul>
</%def>
<%def name="get_feedback()">
<i> Participation in the hinting system is strictly optional, and will not influence
your grade. </i>
<br />
Help us improve our hinting system. Start by picking one of your previous incorrect answers from below:
<br /><br />
<div id="answer-tabs">
<ul>
% for index, answer in index_to_answer.items():
<li><a href="#previous-answer-${index}"> ${answer} </a></li>
% endfor
</ul>
% for index, answer in index_to_answer.items():
<div class = "previous-answer" id="previous-answer-${index}">
% 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}?
<br />
% for hint_text, hint_pk in index_to_hints[index]:
<input class="vote" data-answer="${index}" data-hintno="${hint_pk}" type="button" value="Vote">
${hint_text}
<br />
% 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.
<textarea cols="50" style="height:100px" class="custom-hint" id="custom-hint-${index}">
What would you say to help someone who got this wrong answer?
(Don't give away the answer, please.)
</textarea>
<input class="submit-hint" data-answer="${index}" type="button" value="submit">
</div>
% endfor
</div>
</%def>
<%def name="show_votes()">
Thank you for voting!
<br />
% for hint, votes in hint_and_votes:
<span style="color:green"> ${votes} votes. </span>
${hint}
<br />
% endfor
</%def>
<%def name="simple_message()">
${message}
</%def>
% 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
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