Commit b5e1d57e by Felix Sun

Merge pull request #442 from edx/felix/formula-hints

Crowdsourced Hints - "0.2 release"
parents 02cb0b48 444f51d6
......@@ -915,7 +915,26 @@ class NumericalResponse(LoncapaResponse):
else:
return CorrectMap(self.answer_id, 'incorrect')
# TODO: add check_hint_condition(self, hxml_set, student_answers)
def compare_answer(self, ans1, ans2):
"""
Outside-facing function that lets us compare two numerical answers,
with this problem's tolerance.
"""
return compare_with_tolerance(
evaluator({}, {}, ans1),
evaluator({}, {}, ans2),
self.tolerance
)
def validate_answer(self, answer):
"""
Returns whether this answer is in a valid form.
"""
try:
evaluator(dict(), dict(), answer)
return True
except (StudentInputError, UndefinedVariable):
return False
def get_answers(self):
return {self.answer_id: self.correct_answer}
......@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse):
self.correct_answer, given, self.samples)
return CorrectMap(self.answer_id, correctness)
def check_formula(self, expected, given, samples):
variables = samples.split('@')[0].split(',')
numsamples = int(samples.split('@')[1].split('#')[1])
sranges = zip(*map(lambda x: map(float, x.split(",")),
samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges))
for _ in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context))
student_variables = {}
# ranges give numerical ranges for testing
for var in ranges:
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value
student_variables[str(var)] = value
# log.debug('formula: instructor_vars=%s, expected=%s' %
# (instructor_variables,expected))
# Call `evaluator` on the instructor's answer and get a number
instructor_result = evaluator(
instructor_variables, {},
expected, case_sensitive=self.case_sensitive
)
def tupleize_answers(self, answer, var_dict_list):
"""
Takes in an answer and a list of dictionaries mapping variables to values.
Each dictionary represents a test case for the answer.
Returns a tuple of formula evaluation results.
"""
out = []
for var_dict in var_dict_list:
try:
# log.debug('formula: student_vars=%s, given=%s' %
# (student_variables,given))
# Call `evaluator` on the student's answer; look for exceptions
student_result = evaluator(
student_variables,
{},
given,
case_sensitive=self.case_sensitive
)
out.append(evaluator(
var_dict,
dict(),
answer,
case_sensitive=self.case_sensitive,
))
except UndefinedVariable as uv:
log.debug(
'formularesponse: undefined variable in given=%s',
given
)
'formularesponse: undefined variable in formula=%s' % answer)
raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer"
)
......@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse):
# If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve))
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
cgi.escape(answer))
except Exception as err:
# traceback.print_exc()
log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
cgi.escape(answer))
return out
# No errors in student's response--actually test for correctness
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return "incorrect"
return "correct"
def randomize_variables(self, samples):
"""
Returns a list of dictionaries mapping variables to random values in range,
as expected by tupleize_answers.
"""
variables = samples.split('@')[0].split(',')
numsamples = int(samples.split('@')[1].split('#')[1])
sranges = zip(*map(lambda x: map(float, x.split(",")),
samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges))
out = []
for i in range(numsamples):
var_dict = {}
# ranges give numerical ranges for testing
for var in ranges:
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
value = random.uniform(*ranges[var])
var_dict[str(var)] = value
out.append(var_dict)
return out
def check_formula(self, expected, given, samples):
"""
Given an expected answer string, a given (student-produced) answer
string, and a samples string, return whether the given answer is
"correct" or "incorrect".
"""
var_dict_list = self.randomize_variables(samples)
student_result = self.tupleize_answers(given, var_dict_list)
instructor_result = self.tupleize_answers(expected, var_dict_list)
correct = all(compare_with_tolerance(student, instructor, self.tolerance)
for student, instructor in zip(student_result, instructor_result))
if correct:
return "correct"
else:
return "incorrect"
def compare_answer(self, ans1, ans2):
"""
An external interface for comparing whether a and b are equal.
"""
internal_result = self.check_formula(ans1, ans2, self.samples)
return internal_result == "correct"
def validate_answer(self, answer):
"""
Returns whether this answer is in a valid form.
"""
var_dict_list = self.randomize_variables(self.samples)
try:
self.tupleize_answers(answer, var_dict_list)
return True
except StudentInputError:
return False
def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word
......
......@@ -496,6 +496,20 @@ class FormulaResponseTest(ResponseTest):
input_dict = {'1_2_1': '1/0'}
self.assertRaises(StudentInputError, problem.grade_answers, input_dict)
def test_validate_answer(self):
"""
Makes sure that validate_answer works.
"""
sample_dict = {'x': (1, 2)}
problem = self.build_problem(
sample_dict=sample_dict,
num_samples=10,
tolerance="1%",
answer="x"
)
self.assertTrue(problem.responders.values()[0].validate_answer('14*x'))
self.assertFalse(problem.responders.values()[0].validate_answer('3*y+2*x'))
class StringResponseTest(ResponseTest):
from capa.tests.response_xml_factory import StringResponseXMLFactory
......@@ -915,6 +929,20 @@ class NumericalResponseTest(ResponseTest):
with self.assertRaisesRegexp(StudentInputError, msg_regex):
problem.grade_answers({'1_2_1': 'foobar'})
def test_compare_answer(self):
"""Tests the answer compare function."""
problem = self.build_problem(answer="42")
responder = problem.responders.values()[0]
self.assertTrue(responder.compare_answer('48', '8*6'))
self.assertFalse(responder.compare_answer('48', '9*5'))
def test_validate_answer(self):
"""Tests the answer validation function."""
problem = self.build_problem(answer="42")
responder = problem.responders.values()[0]
self.assertTrue(responder.validate_answer('23.5'))
self.assertFalse(responder.validate_answer('fish'))
class CustomResponseTest(ResponseTest):
from capa.tests.response_xml_factory import CustomResponseXMLFactory
......
......@@ -7,52 +7,6 @@
background: rgb(253, 248, 235);
}
#answer-tabs {
background: #FFFFFF;
border: none;
margin-bottom: 20px;
padding-bottom: 20px;
}
#answer-tabs .ui-widget-header {
border-bottom: 1px solid #DCDCDC;
background: #FDF8EB;
}
#answer-tabs .ui-tabs-nav .ui-state-default {
border: 1px solid #DCDCDC;
background: #E6E6E3;
margin-bottom: 0px;
}
#answer-tabs .ui-tabs-nav .ui-state-default:hover {
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-active:hover {
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-active {
border: 1px solid #DCDCDC;
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-active a {
color: #222222;
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-default a:hover {
color: #222222;
background: #FFFFFF;
}
#answer-tabs .custom-hint {
height: 100px;
width: 100%;
}
.hint-inner-container {
padding-left: 15px;
padding-right: 15px;
......@@ -63,3 +17,24 @@
padding-top: 0px !important;
padding-bottom: 0px !important;
}
.wizard-view {
float: left;
width: 790px;
margin-right: 10px;
}
.wizard-container {
width: 3000px;
-webkit-transition:all 1.0s ease-in-out;
-moz-transition:all 1.0s ease-in-out;
-o-transition:all 1.0s ease-in-out;
transition:all 1.0s ease-in-out;
}
.wizard-viewbox {
width: 800px;
overflow: hidden;
position: relative;
}
<li id="vert-0" data-id="i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0">
<section class="xmodule_display xmodule_CrowdsourceHinterModule" data-type="Hinter" id="hinter-root">
<section class="xmodule_display xmodule_CapaModule" data-type="Problem" id="problem">
<section id="problem_i4x-Me-19_002-problem-Numerical_Input" class="problems-wrapper" data-problem-id="i4x://Me/19.002/problem/Numerical_Input" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/problem/Numerical_Input" data-progress_status="done" data-progress_detail="1/1">
<h2 class="problem-header">
Numerical Input
</h2>
<section class="problem-progress">(1/1 points)</section>
<section class="problem">
<div><p>The answer is 2*x^2*y + 5
</p><span><br><span> Answer =</span>
<section id="inputtype_i4x-Me-19_002-problem-Numerical_Input_2_1" class="text-input-dynamath capa_inputtype ">
<div class="correct " id="status_i4x-Me-19_002-problem-Numerical_Input_2_1">
<input type="text" name="input_i4x-Me-19_002-problem-Numerical_Input_2_1" id="input_i4x-Me-19_002-problem-Numerical_Input_2_1" aria-describedby="answer_i4x-Me-19_002-problem-Numerical_Input_2_1" value="2*x^2*y +5" class="math" size="40">
</div></section></span>
<input type="file" />
<section class="solution-span"><span id="solution_i4x-Me-19_002-problem-Numerical_Input_solution_1"></span></section></div>
<section class="action">
<input type="hidden" name="problem_id" value="Numerical Input">
<input class="check Check" type="button" value="Check">
<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>
</section>
</section>
</section>
</section>
<div id="i4x_Me_19_002_problem_Numerical_Input_setup"></div>
<section class="crowdsource-wrapper" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0" data-child-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/problem/Numerical_Input" style="display: none;"> </section>
</section>
</li>
\ No newline at end of file
......@@ -125,9 +125,10 @@ describe 'Problem', ->
expect(@problem.bind).toHaveBeenCalled()
describe 'check_fd', ->
xit 'should have specs written for this functionality', ->
xit 'should have more specs written for this functionality', ->
expect(false)
describe 'check', ->
beforeEach ->
@problem = new Problem($('.xmodule_display'))
......@@ -137,6 +138,15 @@ describe 'Problem', ->
@problem.check()
expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2'
it 'log the problem_graded event, after the problem is done grading.', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
response =
success: 'correct'
contents: 'mock grader response'
callback(response)
@problem.check()
expect(Logger.log).toHaveBeenCalledWith 'problem_graded', ['foo=1&bar=2', 'mock grader response'], @problem.url
it 'submit the answer for check', ->
spyOn $, 'postWithPrefix'
@problem.check()
......
describe 'Crowdsourced hinter', ->
beforeEach ->
window.update_schematics = ->
jasmine.stubRequests()
# note that the fixturesPath is set in spec/helper.coffee
loadFixtures 'crowdsource_hinter.html'
@hinter = new Hinter($('#hinter-root'))
describe 'high-level integration tests', ->
# High-level, happy-path tests for integration with capa problems.
beforeEach ->
# Make a more thorough $.postWithPrefix mock.
spyOn($, 'postWithPrefix').andCallFake( ->
last_argument = arguments[arguments.length - 1]
if typeof last_argument == 'function'
response =
success: 'incorrect'
contents: 'mock grader response'
last_argument(response)
)
@problem = new Problem($('#problem'))
@problem.bind()
it 'knows when a capa problem is graded, using check.', ->
@problem.answers = 'test answer'
@problem.check()
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'test answer', jasmine.any(Function))
it 'knows when a capa problem is graded usig check_fd.', ->
spyOn($, 'ajaxWithPrefix').andCallFake((url, settings) ->
response =
success: 'incorrect'
contents: 'mock grader response'
settings.success(response)
)
@problem.answers = 'test answer'
@problem.check_fd()
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'test answer', jasmine.any(Function))
describe 'capture_problem', ->
beforeEach ->
spyOn($, 'postWithPrefix').andReturn(null)
it 'gets hints for an incorrect answer', ->
data = ['some answers', '<thing class="incorrect">']
@hinter.capture_problem('problem_graded', data, 'fake element')
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'some answers', jasmine.any(Function))
it 'gets feedback for a correct answer', ->
data = ['some answers', '<thing class="correct">']
@hinter.capture_problem('problem_graded', data, 'fake element')
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_feedback", 'some answers', jasmine.any(Function))
......@@ -19,7 +19,6 @@ class @Problem
problem_prefix = @element_id.replace(/problem_/,'')
@inputs = @$("[id^=input_#{problem_prefix}_]")
@$('section.action input:button').click @refreshAnswers
@$('section.action input.check').click @check_fd
@$('section.action input.reset').click @reset
......@@ -247,6 +246,7 @@ class @Problem
@updateProgress response
else
@gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @url
if not abort_submission
$.ajaxWithPrefix("#{@url}/problem_check", settings)
......
......@@ -29,42 +29,73 @@ class @Hinter
$(selector, @el)
bind: =>
window.update_schematics()
@$('input.vote').click @vote
@$('input.submit-hint').click @submit_hint
@$('.custom-hint').click @clear_default_text
@$('#answer-tabs').tabs({active: 0})
@$('.expand-goodhint').click @expand_goodhint
@$('.expand').click @expand
@$('.wizard-link').click @wizard_link_handle
@$('.answer-choice').click @answer_choice_handle
expand_goodhint: =>
if @$('.goodhint').css('display') == 'none'
@$('.goodhint').css('display', 'block')
expand: (eventObj) =>
# Expand a hidden div.
target = @$('#' + @$(eventObj.currentTarget).data('target'))
if @$(target).css('display') == 'none'
@$(target).css('display', 'block')
else
@$('.goodhint').css('display', 'none')
@$(target).css('display', 'none')
# Fix positioning errors with the bottom class.
@set_bottom_links()
vote: (eventObj) =>
# Make an ajax request with the user's vote.
target = @$(eventObj.currentTarget)
post_json = {'answer': target.data('answer'), 'hint': target.data('hintno')}
all_pks = @$('#pk-list').attr('data-pk-list')
post_json = {'answer': target.attr('data-answer'), 'hint': target.data('hintno'), 'pk_list': all_pks}
$.postWithPrefix "#{@url}/vote", post_json, (response) =>
@render(response.contents)
submit_hint: (eventObj) =>
target = @$(eventObj.currentTarget)
textarea_id = '#custom-hint-' + target.data('answer')
post_json = {'answer': target.data('answer'), 'hint': @$(textarea_id).val()}
# Make an ajax request with the user's new hint.
textarea = $('.custom-hint')
if @answer == ''
# The user didn't choose an answer, somehow. Do nothing.
return
post_json = {'answer': @answer, 'hint': textarea.val()}
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
@render(response.contents)
clear_default_text: (eventObj) =>
# Remove placeholder text in the hint submission textbox.
target = @$(eventObj.currentTarget)
if target.data('cleared') == undefined
target.val('')
target.data('cleared', true)
wizard_link_handle: (eventObj) =>
# Move to another wizard view, based on the link that the user clicked.
target = @$(eventObj.currentTarget)
@go_to(target.attr('dest'))
answer_choice_handle: (eventObj) =>
# A special case of wizard_link_handle - we need to track a state variable,
# the answer that the user chose.
@answer = @$(eventObj.target).attr('value')
@$('#blank-answer').html(@answer)
@go_to('p3')
set_bottom_links: =>
# Makes each .bottom class stick to the bottom of .wizard-viewbox
@$('.bottom').css('margin-top', '0px')
viewbox_height = parseInt(@$('.wizard-viewbox').css('height'), 10)
@$('.bottom').each((index, obj) ->
view_height = parseInt($(obj).parent().css('height'), 10)
$(obj).css('margin-top', (viewbox_height - view_height) + 'px')
)
render: (content) ->
if content
# Trim leading and trailing whitespace
content = content.replace /^\s+|\s+$/g, ""
content = content.trim()
if content
@el.html(content)
......@@ -74,3 +105,37 @@ class @Hinter
@$('#previous-answer-0').css('display', 'inline')
else
@el.hide()
# Initialize the answer choice - remembers which answer the user picked on
# p2 when he submits a hint on p3.
@answer = ''
# Determine whether the browser supports CSS3 transforms.
styles = document.body.style
if styles.WebkitTransform == '' or styles.transform == ''
@go_to = @transform_go_to
else
@go_to = @legacy_go_to
# Make the correct wizard view show up.
hints_exist = @$('#hints-exist').html() == 'True'
if hints_exist
@go_to('p1')
else
@go_to('p2')
transform_go_to: (view_id) ->
# Switch wizard views using sliding transitions.
id_to_index = {
'p1': 0,
'p2': 1,
'p3': 2,
}
translate_string = 'translateX(' +id_to_index[view_id] * -1 * parseInt($('#' + view_id).css('width'), 10) + 'px)'
@$('.wizard-container').css('transform', translate_string)
@$('.wizard-container').css('-webkit-transform', translate_string)
@set_bottom_links()
legacy_go_to: (view_id) ->
# For older browsers - switch wizard views by changing the screen.
@$('.wizard-view').css('display', 'none')
@$('#' + view_id).css('display', 'block')
@set_bottom_links()
\ No newline at end of file
......@@ -3,94 +3,137 @@
<%def name="get_hint()">
% if best_hint != '':
% if len(hints) > 0:
<h4> Hints from students who made similar mistakes: </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>
% for hint in hints:
<li> ${hint} </li>
% endfor
</ul>
% endif
</ul>
</%def>
<%def name="get_feedback()">
<p><em> Participation in the hinting system is strictly optional, and will not influence your grade. </em></p>
<p>
Help your classmates by writing hints for this problem. Start by picking one of your previous incorrect answers from below:
</p>
<%
def unspace(in_str):
"""
HTML id's can't have spaces in them. This little function
removes spaces.
"""
return ''.join(in_str.split())
# Make a list of all hints shown. (This is fed back to the site as pk_list.)
# At the same time, determine whether any hints were shown at all.
# If the user never saw hints, don't ask him to vote.
import json
hints_exist = False
pk_list = []
for answer, pk_dict in answer_to_hints.items():
if len(pk_dict) > 0:
hints_exist = True
for pk, hint_text in pk_dict.items():
pk_list.append([answer, pk])
json_pk_list = json.dumps(pk_list)
%>
<!-- Tells coffeescript whether there are hints to show. -->
<span id="hints-exist" style="display:none">${hints_exist}</span>
<div class="wizard-viewbox"><div class="wizard-container">
<div class="wizard-view" id="p1">
<p>
<em> Optional. </em> Help us improve our hints! Which hint was most helpful to you?
</p>
<div id="answer-tabs">
<ul>
% for index, answer in index_to_answer.items():
<li><a href="#previous-answer-${index}"> ${answer} </a></li>
% endfor
</ul>
<div id="pk-list" data-pk-list='${json_pk_list}' style="display:none"> </div>
% for index, answer in index_to_answer.items():
<div class = "previous-answer" id="previous-answer-${index}">
<div class = "hint-inner-container">
% if index in index_to_hints and len(index_to_hints[index]) > 0:
<p>
Which hint would be most effective to show a student who also got ${answer}?
</p>
% for hint_text, hint_pk in index_to_hints[index]:
<p>
<input class="vote" data-answer="${index}" data-hintno="${hint_pk}" type="button" value="Vote"/>
${hint_text}
</p>
% endfor
<p>
Don't like any of the hints above? You can also submit your own.
</p>
% endif
<p>
What hint would you give a student who made the same mistake you did? Please don't give away the answer.
</p>
<textarea cols="50" 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>
<br/><br/>
<input class="submit-hint" data-answer="${index}" type="button" value="submit">
</div></div>
% endfor
% for answer, pk_dict in answer_to_hints.items():
% for hint_pk, hint_text in pk_dict.items():
<p>
<input class="vote" data-answer="${answer}" data-hintno="${hint_pk}" type="button" value="Vote">
${hint_text}
</p>
% endfor
% endfor
<p>
Don't like any of the hints above?
<a class="wizard-link" dest="p2" href="javascript: void(0);">
Write your own!
</a></p>
</div>
<p>Read about <a class="expand-goodhint" href="javascript:void(0);">what makes a good hint</a>.</p>
<div class="goodhint" style="display:none">
<h4>What makes a good hint?</h4>
<div class="wizard-view" id="p2">
% if hints_exist:
<p>
Choose the incorrect answer for which you want to write a hint:
</p>
% else:
<p>
<em>Optional.</em> Help other students by submitting a hint! Pick one of your previous
answers for which you would like to write a hint:
</p>
% endif
% for answer in user_submissions:
<a class="answer-choice" href="javascript: void(0)" value="${answer}">${answer}</a><br />
% endfor
% if hints_exist:
<p class="bottom">
<a href="javascript: void(0);" class="wizard-link" dest="p1"> Back </a>
</p>
% endif
<p>It depends on the type of problem you ran into. For stupid errors --
an arithmetic error or similar -- simply letting the student you'll be
helping to check their signs is sufficient.</p>
</div>
<p>For deeper errors of understanding, the best hints allow students to
discover a contradiction in how they are thinking about the
problem. An example that clearly demonstrates inconsistency or
<a href="http://en.wikipedia.org/wiki/Cognitive_dissonance" target="_blank"> cognitive dissonace </a>
is ideal, although in most cases, not possible.</p>
<div class="wizard-view" id="p3">
<p>
Good hints either:
<ul>
<li> Point out the specific misunderstanding your classmate might have </li>
<li> Point to concepts or theories where your classmates might have a
misunderstanding </li>
<li> Show simpler, analogous examples. </li>
<li> Provide references to relevant parts of the text </li>
</ul>
Write a hint for other students who get the wrong answer of <span id="blank-answer"></span>.
</p>
<p>Read about <a class="expand" data-target="goodhint" href="javascript:void(0);">what makes a good hint</a>.</p>
<textarea cols="50" class="custom-hint" data-answer="${answer}" style="height: 200px">
Write your hint here. Please don't give away the correct answer.
</textarea>
<br /><br />
<input class="submit-hint" data-answer="${answer}" type="button" value="Submit">
<div id="goodhint" style="display:none">
<h4>What makes a good hint?</h4>
<p>It depends on the type of problem you ran into. For stupid errors --
an arithmetic error or similar -- simply letting the student you'll be
helping to check their signs is sufficient.</p>
<p>For deeper errors of understanding, the best hints allow students to
discover a contradiction in how they are thinking about the
problem. An example that clearly demonstrates inconsistency or
<a href="http://en.wikipedia.org/wiki/Cognitive_dissonance" target="_blank"> cognitive dissonace </a>
is ideal, although in most cases, not possible.</p>
<p>
Good hints either:
<ul>
<li> Point out the specific misunderstanding your classmate might have </li>
<li> Point to concepts or theories where your classmates might have a
misunderstanding </li>
<li> Show simpler, analogous examples. </li>
<li> Provide references to relevant parts of the text </li>
</ul>
</p>
<p>Still, remember even a crude hint -- virtually anything short of
giving away the answer -- is better than no hint.</p>
<p>
<a href="http://www.apa.org/education/k12/misconceptions.aspx?item=2" target="_blank">Learn even more</a>
</p>
</div>
<p class="bottom">
<a href="javascript: void(0);" class="wizard-link" dest="p2"> Back </a>
</p>
</div>
<!-- Close wizard contaner and wizard viewbox. -->
</div></div>
<p>Still, remember even a crude hint -- virtually anything short of
giving away the answer -- is better than no hint.</p>
<p>
<a href="http://www.apa.org/education/k12/misconceptions.aspx?item=2" target="_blank">Learn even more</a>
</p> </div>
</%def>
......@@ -124,6 +167,10 @@ What would you say to help someone who got this wrong answer?
${simple_message()}
% endif
% if op == "error":
${error}
% endif
% if op == "vote":
${show_votes()}
% endif
......
"""
Views for hint management.
Along with the crowdsource_hinter xmodule, this code is still
experimental, and should not be used in new courses, yet.
Get to these views through courseurl/hint_manager.
For example: https://courses.edx.org/courses/MITx/2.01x/2013_Spring/hint_manager
These views will only be visible if MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
"""
import json
......@@ -15,12 +17,17 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
from courseware.models import XModuleContentField
import courseware.module_render as module_render
import courseware.model_data as model_data
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
@ensure_csrf_cookie
def hint_manager(request, course_id):
"""
The URL landing function for all calls to the hint manager, both POST and GET.
"""
try:
get_course_with_access(request.user, course_id, 'staff', depth=None)
except Http404:
......@@ -28,24 +35,29 @@ def hint_manager(request, course_id):
return HttpResponse(out)
if request.method == 'GET':
out = get_hints(request, course_id, 'mod_queue')
return render_to_response('courseware/hint_manager.html', out)
out.update({'error': ''})
return render_to_response('instructor/hint_manager.html', out)
field = request.POST['field']
if not (field == 'mod_queue' or field == 'hints'):
# Invalid field. (Don't let users continue - they may overwrite other db's)
out = 'Error in hint manager - an invalid field was accessed.'
return HttpResponse(out)
if request.POST['op'] == 'delete hints':
delete_hints(request, course_id, field)
if request.POST['op'] == 'switch fields':
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))
switch_dict = {
'delete hints': delete_hints,
'switch fields': lambda *args: None, # Takes any number of arguments, returns None.
'change votes': change_votes,
'add hint': add_hint,
'approve': approve,
}
# Do the operation requested, and collect any error messages.
error_text = switch_dict[request.POST['op']](request, course_id, field)
if error_text is None:
error_text = ''
render_dict = get_hints(request, course_id, field)
render_dict.update({'error': error_text})
rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict)
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
......@@ -165,7 +177,13 @@ def change_votes(request, course_id, field):
Updates the number of votes.
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.
See `delete_hints`.
Example `request.POST`:
{'op': 'delete_hints',
'field': 'mod_queue',
1: ['problem_whatever', '42.0', '3', 42],
2: ['problem_whatever', '32.5', '12', 9001]}
"""
for key in request.POST:
......@@ -193,6 +211,18 @@ def add_hint(request, course_id, field):
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, course_id=course_id)
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)
......@@ -214,6 +244,8 @@ def approve(request, course_id, field):
hint list. POST:
op, field
(some number) -> [problem, answer, pk]
The numbered fields are analogous to those in `delete_hints` and `change_votes`.
"""
for key in request.POST:
......
......@@ -2,6 +2,7 @@ import json
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from mock import patch, MagicMock
from courseware.models import XModuleContentField
from courseware.tests.factories import ContentFactory
......@@ -137,16 +138,45 @@ class HintManagerTest(ModuleStoreTestCase):
"""
Check that instructors can add new hints.
"""
# Because add_hint accesses the xmodule, this test requires a bunch
# of monkey patching.
hinter = MagicMock()
hinter.validate_answer = lambda string: True
request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint',
'problem': self.problem_id,
'answer': '3.14',
'hint': 'This is a new hint.'})
view.add_hint(post, self.course_id, 'mod_queue')
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('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
......
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%namespace name="content" file="/courseware/hint_manager_inner.html"/>
<%namespace name="content" file="/instructor/hint_manager_inner.html"/>
<%block name="headextra">
......
......@@ -4,6 +4,7 @@
<h1> ${field_label} </h1>
Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label}</a>
<p style="color:red"> ${error} </p>
% for definition_id in all_hints:
<h2> Problem: ${id_to_name[definition_id]} </h2>
......@@ -36,6 +37,7 @@ Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label
<br />
% endfor
<p style="color:red"> ${error} </p>
<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>
% if field == 'mod_queue':
......
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