Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
b5e1d57e
Commit
b5e1d57e
authored
Aug 27, 2013
by
Felix Sun
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #442 from edx/felix/formula-hints
Crowdsourced Hints - "0.2 release"
parents
02cb0b48
444f51d6
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
850 additions
and
292 deletions
+850
-292
common/lib/capa/capa/responsetypes.py
+94
-44
common/lib/capa/capa/tests/test_responsetypes.py
+28
-0
common/lib/xmodule/xmodule/crowdsource_hinter.py
+157
-71
common/lib/xmodule/xmodule/css/crowdsource_hinter/display.scss
+21
-46
common/lib/xmodule/xmodule/js/fixtures/crowdsource_hinter.html
+53
-0
common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
+11
-1
common/lib/xmodule/xmodule/js/spec/crowdsource_hinter/display_spec.coffee
+54
-0
common/lib/xmodule/xmodule/js/src/capa/display.coffee
+1
-1
common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
+78
-12
common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
+153
-28
common/templates/hinter_display.html
+119
-72
lms/djangoapps/instructor/hint_manager.py
+47
-15
lms/djangoapps/instructor/tests/test_hint_manager.py
+31
-1
lms/templates/instructor/hint_manager.html
+1
-1
lms/templates/instructor/hint_manager_inner.html
+2
-0
No files found.
common/lib/capa/capa/responsetypes.py
View file @
b5e1d57e
...
...
@@ -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
...
...
common/lib/capa/capa/tests/test_responsetypes.py
View file @
b5e1d57e
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/crowdsource_hinter.py
View file @
b5e1d57e
...
...
@@ -7,15 +7,18 @@ Currently experimental - not for instructor use, yet.
import
logging
import
json
import
random
import
copy
from
pkg_resources
import
resource_string
from
lxml
import
etree
from
xmodule.x_module
import
XModule
from
xmodule.
xml_module
import
Xml
Descriptor
from
xmodule.
raw_module
import
Raw
Descriptor
from
xblock.core
import
Scope
,
String
,
Integer
,
Boolean
,
Dict
,
List
from
capa.responsetypes
import
FormulaResponse
from
django.utils.html
import
escape
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -37,10 +40,15 @@ class CrowdsourceHinterFields(object):
mod_queue
=
Dict
(
help
=
'A dictionary containing hints still awaiting approval'
,
scope
=
Scope
.
content
,
default
=
{})
hint_pk
=
Integer
(
help
=
'Used to index hints.'
,
scope
=
Scope
.
content
,
default
=
0
)
# A list of previous answers this student made to this problem.
# Of the form [answer, [hint_pk_1, hint_pk_2, hint_pk_3]] for each problem. hint_pk's are
# None if the hint was not given.
previous_answers
=
List
(
help
=
'A list of previous submissions.'
,
scope
=
Scope
.
user_state
,
default
=
[])
# A list of previous hints that a student viewed.
# Of the form [answer, [hint_pk_1, ...]] for each problem.
# Sorry about the variable name - I know it's confusing.
previous_answers
=
List
(
help
=
'A list of hints viewed.'
,
scope
=
Scope
.
user_state
,
default
=
[])
# user_submissions actually contains a list of previous answers submitted.
# (Originally, preivous_answers did this job, hence the name confusion.)
user_submissions
=
List
(
help
=
'A list of previous submissions'
,
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
)
...
...
@@ -68,6 +76,26 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
def
__init__
(
self
,
*
args
,
**
kwargs
):
XModule
.
__init__
(
self
,
*
args
,
**
kwargs
)
# We need to know whether we are working with a FormulaResponse problem.
try
:
responder
=
self
.
get_display_items
()[
0
]
.
lcp
.
responders
.
values
()[
0
]
except
(
IndexError
,
AttributeError
):
log
.
exception
(
'Unable to find a capa problem child.'
)
return
self
.
is_formula
=
isinstance
(
self
,
FormulaResponse
)
if
self
.
is_formula
:
self
.
answer_to_str
=
self
.
formula_answer_to_str
else
:
self
.
answer_to_str
=
self
.
numerical_answer_to_str
# compare_answer is expected to return whether its two inputs are close enough
# to be equal, or raise a StudentInputError if one of the inputs is malformatted.
if
hasattr
(
responder
,
'compare_answer'
)
and
hasattr
(
responder
,
'validate_answer'
):
self
.
compare_answer
=
responder
.
compare_answer
self
.
validate_answer
=
responder
.
validate_answer
else
:
# This response type is not supported!
log
.
exception
(
'Response type not supported for hinting: '
+
str
(
responder
))
def
get_html
(
self
):
"""
...
...
@@ -98,14 +126,29 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return
out
def
capa
_answer_to_str
(
self
,
answer
):
def
numerical
_answer_to_str
(
self
,
answer
):
"""
Converts capa answer format to a string representation
Converts capa
numerical
answer format to a string representation
of the answer.
-Lon-capa dependent.
-Assumes that the problem only has one part.
"""
return
str
(
float
(
answer
.
values
()[
0
]))
return
str
(
answer
.
values
()[
0
])
def
formula_answer_to_str
(
self
,
answer
):
"""
Converts capa formula answer into a string.
-Lon-capa dependent.
-Assumes that the problem only has one part.
"""
return
str
(
answer
.
values
()[
0
])
def
get_matching_answers
(
self
,
answer
):
"""
Look in self.hints, and find all answer keys that are "equal with tolerance"
to the input answer.
"""
return
[
key
for
key
in
self
.
hints
if
self
.
compare_answer
(
key
,
answer
)]
def
handle_ajax
(
self
,
dispatch
,
data
):
"""
...
...
@@ -124,6 +167,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
if
out
is
None
:
out
=
{
'op'
:
'empty'
}
elif
'error'
in
out
:
# Error in processing.
out
.
update
({
'op'
:
'error'
})
else
:
out
.
update
({
'op'
:
dispatch
})
return
json
.
dumps
({
'contents'
:
self
.
system
.
render_template
(
'hinter_display.html'
,
out
)})
...
...
@@ -134,51 +180,67 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Called by hinter javascript after a problem is graded as incorrect.
Args:
`data` -- must be interpretable by
capa_
answer_to_str.
`data` -- must be interpretable by answer_to_str.
Output keys:
- 'best_hint' is the hint text with the most votes.
- 'rand_hint_1' and 'rand_hint_2' are two random hints to the answer in `data`.
- 'hints' is a list of hint strings to show to the user.
- 'answer' is the parsed answer that was submitted.
Will record the user's wrong answer in user_submissions, and the hints shown
in previous_answers.
"""
# First, validate our inputs.
try
:
answer
=
self
.
capa_
answer_to_str
(
data
)
except
ValueError
:
answer
=
self
.
answer_to_str
(
data
)
except
(
ValueError
,
AttributeError
)
:
# Sometimes, we get an answer that's just not parsable. Do nothing.
log
.
exception
(
'Answer not parsable: '
+
str
(
data
))
return
# Look for a hint to give.
# Make a local copy of self.hints - this means we only need to do one json unpacking.
# (This is because xblocks storage makes the following command a deep copy.)
local_hints
=
self
.
hints
if
(
answer
not
in
local_hints
)
or
(
len
(
local_hints
[
answer
])
==
0
):
if
not
self
.
validate_answer
(
answer
):
# Answer is not in the right form.
log
.
exception
(
'Answer not valid: '
+
str
(
answer
))
return
if
answer
not
in
self
.
user_submissions
:
self
.
user_submissions
+=
[
answer
]
# For all answers similar enough to our own, accumulate all hints together.
# Also track the original answer of each hint.
matching_answers
=
self
.
get_matching_answers
(
answer
)
matching_hints
=
{}
for
matching_answer
in
matching_answers
:
temp_dict
=
copy
.
deepcopy
(
self
.
hints
[
matching_answer
])
for
key
,
value
in
temp_dict
.
items
():
# Each value now has hint, votes, matching_answer.
temp_dict
[
key
]
=
value
+
[
matching_answer
]
matching_hints
.
update
(
temp_dict
)
# matching_hints now maps pk's to lists of [hint, votes, matching_answer]
# Finally, randomly choose a subset of matching_hints to actually show.
if
not
matching_hints
:
# No hints to give. Return.
self
.
previous_answers
+=
[[
answer
,
[
None
,
None
,
None
]]]
return
# Get the top hint, plus two random hints.
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
=
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
(
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
]]]
return
{
'best_hint'
:
best_hint
,
'rand_hint_1'
:
rand_hint_1
,
'rand_hint_2'
:
rand_hint_2
,
n_hints
=
len
(
matching_hints
)
hints
=
[]
# max(dict) returns the maximum key in dict.
# The key function takes each pk, and returns the number of votes for the
# hint with that pk.
best_hint_index
=
max
(
matching_hints
,
key
=
lambda
pk
:
matching_hints
[
pk
][
1
])
hints
.
append
(
matching_hints
[
best_hint_index
][
0
])
best_hint_answer
=
matching_hints
[
best_hint_index
][
2
]
# The brackets surrounding the index are for backwards compatability purposes.
# (It used to be that each answer was paired with multiple hints in a list.)
self
.
previous_answers
+=
[[
best_hint_answer
,
[
best_hint_index
]]]
for
_
in
xrange
(
min
(
2
,
n_hints
-
1
)):
# Keep making random hints until we hit a target, or run out.
while
True
:
# random.choice randomly chooses an element from its input list.
# (We then unpack the item, in this case data for a hint.)
(
hint_index
,
(
rand_hint
,
_
,
hint_answer
))
=
\
random
.
choice
(
matching_hints
.
items
())
if
rand_hint
not
in
hints
:
break
hints
.
append
(
rand_hint
)
self
.
previous_answers
+=
[[
hint_answer
,
[
hint_index
]]]
return
{
'hints'
:
hints
,
'answer'
:
answer
}
def
get_feedback
(
self
,
data
):
...
...
@@ -188,38 +250,37 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Args:
`data` -- not actually used. (It is assumed that the answer is correct.)
Output keys:
- 'index_to_hints' maps previous answer indices to hints that the user saw earlier.
- 'index_to_answer' maps previous answer indices to the actual answer submitted.
- 'answer_to_hints': a nested dictionary.
answer_to_hints[answer][hint_pk] returns the text of the hint.
- 'user_submissions': the same thing as self.user_submissions. A list of
the answers that the user previously submitted.
"""
# The student got it right.
# Did he submit at least one wrong answer?
if
len
(
self
.
previous_answer
s
)
==
0
:
if
len
(
self
.
user_submission
s
)
==
0
:
# No. Nothing to do here.
return
# Make a hint-voting interface for each wrong answer. The student will only
# be allowed to make one vote / submission, but he can choose which wrong answer
# he wants to look at.
# index_to_hints[previous answer #] = [(hint text, hint pk), + ]
index_to_hints
=
{}
# index_to_answer[previous answer #] = answer text
index_to_answer
=
{}
answer_to_hints
=
{}
# answer_to_hints[answer text][hint pk] -> hint text
# Go through each previous answer, and populate index_to_hints and index_to_answer.
for
i
in
xrange
(
len
(
self
.
previous_answers
)):
answer
,
hints_offered
=
self
.
previous_answers
[
i
]
i
ndex_to_hints
[
i
]
=
[]
index_to_answer
[
i
]
=
answer
i
f
answer
not
in
answer_to_hints
:
answer_to_hints
[
answer
]
=
{}
if
answer
in
self
.
hints
:
# Go through each hint, and add to index_to_hints
for
hint_id
in
hints_offered
:
if
hint_id
is
not
None
:
if
(
hint_id
is
not
None
)
and
(
hint_id
not
in
answer_to_hints
[
answer
])
:
try
:
index_to_hints
[
i
]
.
append
((
self
.
hints
[
answer
][
str
(
hint_id
)][
0
],
hint_id
))
answer_to_hints
[
answer
][
hint_id
]
=
self
.
hints
[
answer
][
str
(
hint_id
)][
0
]
except
KeyError
:
# Sometimes, the hint that a user saw will have been deleted by the instructor.
continue
return
{
'index_to_hints'
:
index_to_hints
,
'index_to_answer'
:
index_to_answer
}
return
{
'answer_to_hints'
:
answer_to_hints
,
'user_submissions'
:
self
.
user_submissions
}
def
tally_vote
(
self
,
data
):
"""
...
...
@@ -227,31 +288,51 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Args:
`data` -- expected to have the following keys:
'answer':
ans_no (index in previous_answers)
'answer':
text of answer we're voting on
'hint': hint_pk
'pk_list': A list of [answer, pk] pairs, each of which representing a hint.
We will return a list of how many votes each hint in the list has so far.
It's up to the browser to specify which hints to return vote counts for.
Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs.
"""
if
self
.
user_voted
:
return
{}
ans_no
=
int
(
data
[
'answer'
])
hint_no
=
str
(
data
[
'hint'
])
answer
=
self
.
previous_answers
[
ans_no
][
0
]
return
{
'error'
:
'Sorry, but you have already voted!'
}
ans
=
data
[
'answer'
]
if
not
self
.
validate_answer
(
ans
):
# Uh oh. Invalid answer.
log
.
exception
(
'Failure in hinter tally_vote: Unable to parse answer: {ans}'
.
format
(
ans
=
ans
))
return
{
'error'
:
'Failure in voting!'
}
hint_pk
=
str
(
data
[
'hint'
])
# We use temp_dict because we need to do a direct write for the database to update.
temp_dict
=
self
.
hints
temp_dict
[
answer
][
hint_no
][
1
]
+=
1
try
:
temp_dict
[
ans
][
hint_pk
][
1
]
+=
1
except
KeyError
:
log
.
exception
(
'''Failure in hinter tally_vote: User voted for non-existant hint:
Answer={ans} pk={hint_pk}'''
.
format
(
ans
=
ans
,
hint_pk
=
hint_pk
))
return
{
'error'
:
'Failure in voting!'
}
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.
pk_list
=
json
.
loads
(
data
[
'pk_list'
])
hint_and_votes
=
[]
for
hint_no
in
self
.
previous_answers
[
ans_no
][
1
]:
if
hint_no
is
None
:
for
answer
,
vote_pk
in
pk_list
:
if
not
self
.
validate_answer
(
answer
):
log
.
exception
(
'In hinter tally_vote, couldn
\'
t parse {ans}'
.
format
(
ans
=
answer
))
continue
hint_and_votes
.
append
(
temp_dict
[
answer
][
str
(
hint_no
)])
try
:
hint_and_votes
.
append
(
temp_dict
[
answer
][
str
(
vote_pk
)])
except
KeyError
:
log
.
exception
(
'In hinter tally_vote, couldn
\'
t find: {ans}, {vote_pk}'
.
format
(
ans
=
answer
,
vote_pk
=
str
(
vote_pk
)))
# Reset self.previous_answers.
hint_and_votes
.
sort
(
key
=
lambda
pair
:
pair
[
1
],
reverse
=
True
)
# Reset self.previous_answers and user_submissions.
self
.
previous_answers
=
[]
self
.
user_submissions
=
[]
return
{
'hint_and_votes'
:
hint_and_votes
}
def
submit_hint
(
self
,
data
):
...
...
@@ -260,13 +341,17 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Args:
`data` -- expected to have the following keys:
'answer':
answer index in previous_answers
'answer':
text of answer
'hint': text of the new hint that the user is adding
Returns a thank-you message.
"""
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint
=
escape
(
data
[
'hint'
])
answer
=
self
.
previous_answers
[
int
(
data
[
'answer'
])][
0
]
answer
=
data
[
'answer'
]
if
not
self
.
validate_answer
(
answer
):
log
.
exception
(
'Failure in hinter submit_hint: Unable to parse answer: {ans}'
.
format
(
ans
=
answer
))
return
{
'error'
:
'Could not submit answer'
}
# Only allow a student to vote or submit a hint once.
if
self
.
user_voted
:
return
{
'message'
:
'Sorry, but you have already voted!'
}
...
...
@@ -277,9 +362,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
else
:
temp_dict
=
self
.
hints
if
answer
in
temp_dict
:
temp_dict
[
answer
][
s
elf
.
hint_pk
]
=
[
hint
,
1
]
# With one vote (the user himself).
temp_dict
[
answer
][
s
tr
(
self
.
hint_pk
)
]
=
[
hint
,
1
]
# With one vote (the user himself).
else
:
temp_dict
[
answer
]
=
{
s
elf
.
hint_pk
:
[
hint
,
1
]}
temp_dict
[
answer
]
=
{
s
tr
(
self
.
hint_pk
)
:
[
hint
,
1
]}
self
.
hint_pk
+=
1
if
self
.
moderate
==
'True'
:
self
.
mod_queue
=
temp_dict
...
...
@@ -288,10 +373,11 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# Mark the user has having voted; reset previous_answers
self
.
user_voted
=
True
self
.
previous_answers
=
[]
self
.
user_submissions
=
[]
return
{
'message'
:
'Thank you for your hint!'
}
class
CrowdsourceHinterDescriptor
(
CrowdsourceHinterFields
,
Xml
Descriptor
):
class
CrowdsourceHinterDescriptor
(
CrowdsourceHinterFields
,
Raw
Descriptor
):
module_class
=
CrowdsourceHinterModule
stores_state
=
True
...
...
common/lib/xmodule/xmodule/css/crowdsource_hinter/display.scss
View file @
b5e1d57e
...
...
@@ -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
;
}
common/lib/xmodule/xmodule/js/fixtures/crowdsource_hinter.html
0 → 100644
View file @
b5e1d57e
<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
common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
View file @
b5e1d57e
...
...
@@ -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
()
...
...
common/lib/xmodule/xmodule/js/spec/crowdsource_hinter/display_spec.coffee
0 → 100644
View file @
b5e1d57e
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
))
common/lib/xmodule/xmodule/js/src/capa/display.coffee
View file @
b5e1d57e
...
...
@@ -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
)
...
...
common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
View file @
b5e1d57e
...
...
@@ -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
common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
View file @
b5e1d57e
...
...
@@ -2,7 +2,7 @@
Tests the crowdsourced hinter xmodule.
"""
from
mock
import
Mock
from
mock
import
Mock
,
MagicMock
import
unittest
import
copy
...
...
@@ -53,6 +53,7 @@ class CHModuleFactory(object):
@staticmethod
def
create
(
hints
=
None
,
previous_answers
=
None
,
user_submissions
=
None
,
user_voted
=
None
,
moderate
=
None
,
mod_queue
=
None
):
...
...
@@ -85,17 +86,59 @@ class CHModuleFactory(object):
else
:
model_data
[
'previous_answers'
]
=
[
[
'24.0'
,
[
0
,
3
,
4
]],
[
'29.0'
,
[
None
,
None
,
None
]]
[
'29.0'
,
[]]
]
if
user_submissions
is
not
None
:
model_data
[
'user_submissions'
]
=
user_submissions
else
:
model_data
[
'user_submissions'
]
=
[
'24.0'
,
'29.0'
]
if
user_voted
is
not
None
:
model_data
[
'user_voted'
]
=
user_voted
if
moderate
is
not
None
:
model_data
[
'moderate'
]
=
moderate
descriptor
=
Mock
(
weight
=
"1"
)
descriptor
=
Mock
(
weight
=
'1'
)
# Make the descriptor have a capa problem child.
capa_descriptor
=
MagicMock
()
capa_descriptor
.
name
=
'capa'
descriptor
.
get_children
=
lambda
:
[
capa_descriptor
]
# Make a fake capa module.
capa_module
=
MagicMock
()
capa_module
.
lcp
=
MagicMock
()
responder
=
MagicMock
()
def
validate_answer
(
answer
):
""" A mock answer validator - simulates a numerical response"""
try
:
float
(
answer
)
return
True
except
ValueError
:
return
False
responder
.
validate_answer
=
validate_answer
def
compare_answer
(
ans1
,
ans2
):
""" A fake answer comparer """
return
ans1
==
ans2
responder
.
compare_answer
=
compare_answer
capa_module
.
lcp
.
responders
=
{
'responder0'
:
responder
}
capa_module
.
displayable_items
=
lambda
:
[
capa_module
]
system
=
get_test_system
()
# Make the system have a marginally-functional get_module
def
fake_get_module
(
descriptor
):
"""
A fake module-maker.
"""
if
descriptor
.
name
==
'capa'
:
return
capa_module
system
.
get_module
=
fake_get_module
module
=
CrowdsourceHinterModule
(
system
,
descriptor
,
model_data
)
return
module
...
...
@@ -146,11 +189,13 @@ class VerticalWithModulesFactory(object):
@staticmethod
def
next_num
():
"""Increments a global counter for naming."""
CHModuleFactory
.
num
+=
1
return
CHModuleFactory
.
num
@staticmethod
def
create
():
"""Make a vertical."""
model_data
=
{
'data'
:
VerticalWithModulesFactory
.
sample_problem_xml
}
system
=
get_test_system
()
descriptor
=
VerticalDescriptor
.
from_xml
(
VerticalWithModulesFactory
.
sample_problem_xml
,
system
)
...
...
@@ -226,6 +271,24 @@ class CrowdsourceHinterTest(unittest.TestCase):
self
.
assertTrue
(
'Test numerical problem.'
in
out_html
)
self
.
assertTrue
(
'Another test numerical problem.'
in
out_html
)
def
test_numerical_answer_to_str
(
self
):
"""
Tests the get request to string converter for numerical responses.
"""
mock_module
=
CHModuleFactory
.
create
()
get
=
{
'response1'
:
'4'
}
parsed
=
mock_module
.
numerical_answer_to_str
(
get
)
self
.
assertTrue
(
parsed
==
'4'
)
def
test_formula_answer_to_str
(
self
):
"""
Tests the get request to string converter for formula responses.
"""
mock_module
=
CHModuleFactory
.
create
()
get
=
{
'response1'
:
'x*y^2'
}
parsed
=
mock_module
.
formula_answer_to_str
(
get
)
self
.
assertTrue
(
parsed
==
'x*y^2'
)
def
test_gethint_0hint
(
self
):
"""
Someone asks for a hint, when there's no hint to give.
...
...
@@ -235,21 +298,36 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'problem_name'
:
'26.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
print
mock_module
.
previous_answers
self
.
assertTrue
(
out
is
None
)
self
.
assertTrue
(
[
'26.0'
,
[
None
,
None
,
None
]]
in
mock_module
.
previous_answer
s
)
self
.
assertTrue
(
'26.0'
in
mock_module
.
user_submission
s
)
def
test_gethint_unparsable
(
self
):
"""
Someone submits a
hint that cannot be parsed into a flo
at.
Someone submits a
n answer that is in the wrong form
at.
- The answer should not be added to previous_answers.
"""
mock_module
=
CHModuleFactory
.
create
()
old_answers
=
copy
.
deepcopy
(
mock_module
.
previous_answers
)
json_in
=
{
'problem_name'
:
'fish'
}
json_in
=
'blah'
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
(
out
is
None
)
self
.
assertTrue
(
mock_module
.
previous_answers
==
old_answers
)
def
test_gethint_signature_error
(
self
):
"""
Someone submits an answer that cannot be calculated as a float.
Nothing should change.
"""
mock_module
=
CHModuleFactory
.
create
()
old_answers
=
copy
.
deepcopy
(
mock_module
.
previous_answers
)
old_user_submissions
=
copy
.
deepcopy
(
mock_module
.
user_submissions
)
json_in
=
{
'problem1'
:
'fish'
}
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
(
out
is
None
)
self
.
assertTrue
(
mock_module
.
previous_answers
==
old_answers
)
self
.
assertTrue
(
mock_module
.
user_submissions
==
old_user_submissions
)
def
test_gethint_1hint
(
self
):
"""
Someone asks for a hint, with exactly one hint in the database.
...
...
@@ -258,7 +336,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'problem_name'
:
'25.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
(
out
[
'best_hint'
]
==
'Really popular hint'
)
self
.
assertTrue
(
'Really popular hint'
in
out
[
'hints'
])
# Also make sure that the input gets added to user_submissions,
# and that the hint is logged in previous_answers.
self
.
assertTrue
(
'25.0'
in
mock_module
.
user_submissions
)
self
.
assertTrue
([
'25.0'
,
[
'1'
]]
in
mock_module
.
previous_answers
)
def
test_gethint_manyhints
(
self
):
"""
...
...
@@ -271,18 +353,18 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'problem_name'
:
'24.0'
}
out
=
mock_module
.
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
)
self
.
assertTrue
(
'Best hint'
in
out
[
'hints'
])
self
.
assertTrue
(
len
(
out
[
'hints'
])
==
3
)
def
test_getfeedback_0wronganswers
(
self
):
"""
Someone has gotten the problem correct on the first try.
Output should be empty.
"""
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[])
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[]
,
user_submissions
=
[]
)
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
print
out
self
.
assertTrue
(
out
is
None
)
def
test_getfeedback_1wronganswer_nohints
(
self
):
...
...
@@ -294,9 +376,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'26.0'
,
[
None
,
None
,
None
]]])
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
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'
)
self
.
assertTrue
(
out
[
'answer_to_hints'
]
==
{
'26.0'
:
{}})
def
test_getfeedback_1wronganswer_withhints
(
self
):
"""
...
...
@@ -307,8 +387,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'24.0'
,
[
0
,
3
,
None
]]])
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
print
out
[
'index_to_hints'
]
self
.
assertTrue
(
len
(
out
[
'index_to_hints'
][
0
])
==
2
)
self
.
assertTrue
(
len
(
out
[
'answer_to_hints'
][
'24.0'
])
==
2
)
def
test_getfeedback_missingkey
(
self
):
"""
...
...
@@ -319,7 +398,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
previous_answers
=
[[
'24.0'
,
[
0
,
100
,
None
]]])
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
self
.
assertTrue
(
len
(
out
[
'
index_to_hints'
][
0
])
==
1
)
self
.
assertTrue
(
len
(
out
[
'
answer_to_hints'
][
'24.0'
])
==
1
)
def
test_vote_nopermission
(
self
):
"""
...
...
@@ -327,7 +406,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
Should not change any vote tallies.
"""
mock_module
=
CHModuleFactory
.
create
(
user_voted
=
True
)
json_in
=
{
'answer'
:
0
,
'hint'
:
1
}
json_in
=
{
'answer'
:
'24.0'
,
'hint'
:
1
,
'pk_list'
:
json
.
dumps
([[
'24.0'
,
1
],
[
'24.0'
,
3
]])
}
old_hints
=
copy
.
deepcopy
(
mock_module
.
hints
)
mock_module
.
tally_vote
(
json_in
)
self
.
assertTrue
(
mock_module
.
hints
==
old_hints
)
...
...
@@ -339,19 +418,56 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'24.0'
,
[
0
,
3
,
None
]]])
json_in
=
{
'answer'
:
0
,
'hint'
:
3
}
json_in
=
{
'answer'
:
'24.0'
,
'hint'
:
3
,
'pk_list'
:
json
.
dumps
([[
'24.0'
,
0
],
[
'24.0'
,
3
]])
}
dict_out
=
mock_module
.
tally_vote
(
json_in
)
self
.
assertTrue
(
mock_module
.
hints
[
'24.0'
][
'0'
][
1
]
==
40
)
self
.
assertTrue
(
mock_module
.
hints
[
'24.0'
][
'3'
][
1
]
==
31
)
self
.
assertTrue
([
'Best hint'
,
40
]
in
dict_out
[
'hint_and_votes'
])
self
.
assertTrue
([
'Another hint'
,
31
]
in
dict_out
[
'hint_and_votes'
])
def
test_vote_unparsable
(
self
):
"""
A user somehow votes for an unparsable answer.
Should return a friendly error.
(This is an unusual exception path - I don't know how it occurs,
except if you manually make a post request. But, it seems to happen
occasionally.)
"""
mock_module
=
CHModuleFactory
.
create
()
# None means that the answer couldn't be parsed.
mock_module
.
answer_signature
=
lambda
text
:
None
json_in
=
{
'answer'
:
'fish'
,
'hint'
:
3
,
'pk_list'
:
'[]'
}
dict_out
=
mock_module
.
tally_vote
(
json_in
)
print
dict_out
self
.
assertTrue
(
dict_out
==
{
'error'
:
'Failure in voting!'
})
def
test_vote_nohint
(
self
):
"""
A user somehow votes for a hint that doesn't exist.
Should return a friendly error.
"""
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'answer'
:
'24.0'
,
'hint'
:
'25'
,
'pk_list'
:
'[]'
}
dict_out
=
mock_module
.
tally_vote
(
json_in
)
self
.
assertTrue
(
dict_out
==
{
'error'
:
'Failure in voting!'
})
def
test_vote_badpklist
(
self
):
"""
Some of the pk's specified in pk_list are invalid.
Should just skip those.
"""
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'answer'
:
'24.0'
,
'hint'
:
'0'
,
'pk_list'
:
json
.
dumps
([[
'24.0'
,
0
],
[
'24.0'
,
12
]])}
hint_and_votes
=
mock_module
.
tally_vote
(
json_in
)[
'hint_and_votes'
]
self
.
assertTrue
([
'Best hint'
,
41
]
in
hint_and_votes
)
self
.
assertTrue
(
len
(
hint_and_votes
)
==
1
)
def
test_submithint_nopermission
(
self
):
"""
A user tries to submit a hint, but he has already voted.
"""
mock_module
=
CHModuleFactory
.
create
(
user_voted
=
True
)
json_in
=
{
'answer'
:
1
,
'hint'
:
'This is a new hint.'
}
json_in
=
{
'answer'
:
'29.0'
,
'hint'
:
'This is a new hint.'
}
print
mock_module
.
user_voted
mock_module
.
submit_hint
(
json_in
)
print
mock_module
.
hints
...
...
@@ -363,7 +479,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
exist yet.
"""
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'answer'
:
1
,
'hint'
:
'This is a new hint.'
}
json_in
=
{
'answer'
:
'29.0'
,
'hint'
:
'This is a new hint.'
}
mock_module
.
submit_hint
(
json_in
)
self
.
assertTrue
(
'29.0'
in
mock_module
.
hints
)
...
...
@@ -373,13 +489,12 @@ class CrowdsourceHinterTest(unittest.TestCase):
already.
"""
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'25.0'
,
[
1
,
None
,
None
]]])
json_in
=
{
'answer'
:
0
,
'hint'
:
'This is a new hint.'
}
json_in
=
{
'answer'
:
'25.0'
,
'hint'
:
'This is a new hint.'
}
mock_module
.
submit_hint
(
json_in
)
# Make a hint request.
json_in
=
{
'problem name'
:
'25.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
((
out
[
'best_hint'
]
==
'This is a new hint.'
)
or
(
out
[
'rand_hint_1'
]
==
'This is a new hint.'
))
self
.
assertTrue
(
'This is a new hint.'
in
out
[
'hints'
])
def
test_submithint_moderate
(
self
):
"""
...
...
@@ -388,7 +503,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
dict.
"""
mock_module
=
CHModuleFactory
.
create
(
moderate
=
'True'
)
json_in
=
{
'answer'
:
1
,
'hint'
:
'This is a new hint.'
}
json_in
=
{
'answer'
:
'29.0'
,
'hint'
:
'This is a new hint.'
}
mock_module
.
submit_hint
(
json_in
)
self
.
assertTrue
(
'29.0'
not
in
mock_module
.
hints
)
self
.
assertTrue
(
'29.0'
in
mock_module
.
mod_queue
)
...
...
@@ -398,10 +513,20 @@ class CrowdsourceHinterTest(unittest.TestCase):
Make sure that hints are being html-escaped.
"""
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'answer'
:
1
,
'hint'
:
'<script> alert("Trololo"); </script>'
}
json_in
=
{
'answer'
:
'29.0'
,
'hint'
:
'<script> alert("Trololo"); </script>'
}
mock_module
.
submit_hint
(
json_in
)
self
.
assertTrue
(
mock_module
.
hints
[
'29.0'
][
'0'
][
0
]
==
u'<script> alert("Trololo"); </script>'
)
def
test_submithint_unparsable
(
self
):
mock_module
=
CHModuleFactory
.
create
()
mock_module
.
answer_signature
=
lambda
text
:
None
json_in
=
{
'answer'
:
'fish'
,
'hint'
:
'A hint'
}
dict_out
=
mock_module
.
submit_hint
(
json_in
)
print
dict_out
print
mock_module
.
hints
self
.
assertTrue
(
mock_module
.
hints
[
'29.0'
][
0
][
0
]
==
u'<script> alert("Trololo"); </script>'
)
self
.
assertTrue
(
'error'
in
dict_out
)
self
.
assertTrue
(
None
not
in
mock_module
.
hints
)
self
.
assertTrue
(
'fish'
not
in
mock_module
.
hints
)
def
test_template_gethint
(
self
):
"""
...
...
@@ -409,7 +534,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
mock_module
=
CHModuleFactory
.
create
()
def
fake_get_hint
(
get
):
def
fake_get_hint
(
_
):
"""
Creates a rendering dictionary, with which we can test
the templates.
...
...
common/templates/hinter_display.html
View file @
b5e1d57e
...
...
@@ -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
...
...
lms/djangoapps/instructor/hint_manager.py
View file @
b5e1d57e
"""
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
:
...
...
lms/djangoapps/instructor/tests/test_hint_manager.py
View file @
b5e1d57e
...
...
@@ -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
...
...
lms/templates/
courseware
/hint_manager.html
→
lms/templates/
instructor
/hint_manager.html
View file @
b5e1d57e
<
%
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"
>
...
...
lms/templates/
courseware
/hint_manager_inner.html
→
lms/templates/
instructor
/hint_manager_inner.html
View file @
b5e1d57e
...
...
@@ -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':
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment