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
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
815 additions
and
257 deletions
+815
-257
common/lib/capa/capa/responsetypes.py
+93
-43
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
+86
-39
lms/djangoapps/instructor/hint_manager.py
+47
-15
lms/djangoapps/instructor/tests/test_hint_manager.py
+30
-0
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):
...
@@ -915,7 +915,26 @@ class NumericalResponse(LoncapaResponse):
else
:
else
:
return
CorrectMap
(
self
.
answer_id
,
'incorrect'
)
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
):
def
get_answers
(
self
):
return
{
self
.
answer_id
:
self
.
correct_answer
}
return
{
self
.
answer_id
:
self
.
correct_answer
}
...
@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse):
...
@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse):
self
.
correct_answer
,
given
,
self
.
samples
)
self
.
correct_answer
,
given
,
self
.
samples
)
return
CorrectMap
(
self
.
answer_id
,
correctness
)
return
CorrectMap
(
self
.
answer_id
,
correctness
)
def
check_formula
(
self
,
expected
,
given
,
samples
):
def
tupleize_answers
(
self
,
answer
,
var_dict_list
):
variables
=
samples
.
split
(
'@'
)[
0
]
.
split
(
','
)
"""
numsamples
=
int
(
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
1
])
Takes in an answer and a list of dictionaries mapping variables to values.
sranges
=
zip
(
*
map
(
lambda
x
:
map
(
float
,
x
.
split
(
","
)),
Each dictionary represents a test case for the answer.
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
0
]
.
split
(
':'
)))
Returns a tuple of formula evaluation results.
"""
ranges
=
dict
(
zip
(
variables
,
sranges
))
out
=
[]
for
_
in
range
(
numsamples
):
for
var_dict
in
var_dict_list
:
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
)
try
:
try
:
# log.debug('formula: student_vars=%s, given=%s' %
out
.
append
(
evaluator
(
# (student_variables,given))
var_dict
,
dict
(),
# Call `evaluator` on the student's answer; look for exceptions
answer
,
student_result
=
evaluator
(
case_sensitive
=
self
.
case_sensitive
,
student_variables
,
))
{},
given
,
case_sensitive
=
self
.
case_sensitive
)
except
UndefinedVariable
as
uv
:
except
UndefinedVariable
as
uv
:
log
.
debug
(
log
.
debug
(
'formularesponse: undefined variable in given=
%
s'
,
'formularesponse: undefined variable in formula=
%
s'
%
answer
)
given
)
raise
StudentInputError
(
raise
StudentInputError
(
"Invalid input: "
+
uv
.
message
+
" not permitted in answer"
"Invalid input: "
+
uv
.
message
+
" not permitted in answer"
)
)
...
@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse):
...
@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse):
# If non-factorial related ValueError thrown, handle it the same as any other Exception
# If non-factorial related ValueError thrown, handle it the same as any other Exception
log
.
debug
(
'formularesponse: error {0} in formula'
.
format
(
ve
))
log
.
debug
(
'formularesponse: error {0} in formula'
.
format
(
ve
))
raise
StudentInputError
(
"Invalid input: Could not parse '
%
s' as a formula"
%
raise
StudentInputError
(
"Invalid input: Could not parse '
%
s' as a formula"
%
cgi
.
escape
(
given
))
cgi
.
escape
(
answer
))
except
Exception
as
err
:
except
Exception
as
err
:
# traceback.print_exc()
# traceback.print_exc()
log
.
debug
(
'formularesponse: error
%
s in formula'
,
err
)
log
.
debug
(
'formularesponse: error
%
s in formula'
,
err
)
raise
StudentInputError
(
"Invalid input: Could not parse '
%
s' as a formula"
%
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
def
randomize_variables
(
self
,
samples
):
if
not
compare_with_tolerance
(
student_result
,
instructor_result
,
self
.
tolerance
):
"""
return
"incorrect"
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"
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
):
def
strip_dict
(
self
,
d
):
''' Takes a dict. Returns an identical dict, with all non-word
''' 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):
...
@@ -496,6 +496,20 @@ class FormulaResponseTest(ResponseTest):
input_dict
=
{
'1_2_1'
:
'1/0'
}
input_dict
=
{
'1_2_1'
:
'1/0'
}
self
.
assertRaises
(
StudentInputError
,
problem
.
grade_answers
,
input_dict
)
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
):
class
StringResponseTest
(
ResponseTest
):
from
capa.tests.response_xml_factory
import
StringResponseXMLFactory
from
capa.tests.response_xml_factory
import
StringResponseXMLFactory
...
@@ -915,6 +929,20 @@ class NumericalResponseTest(ResponseTest):
...
@@ -915,6 +929,20 @@ class NumericalResponseTest(ResponseTest):
with
self
.
assertRaisesRegexp
(
StudentInputError
,
msg_regex
):
with
self
.
assertRaisesRegexp
(
StudentInputError
,
msg_regex
):
problem
.
grade_answers
({
'1_2_1'
:
'foobar'
})
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
):
class
CustomResponseTest
(
ResponseTest
):
from
capa.tests.response_xml_factory
import
CustomResponseXMLFactory
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.
...
@@ -7,15 +7,18 @@ Currently experimental - not for instructor use, yet.
import
logging
import
logging
import
json
import
json
import
random
import
random
import
copy
from
pkg_resources
import
resource_string
from
pkg_resources
import
resource_string
from
lxml
import
etree
from
lxml
import
etree
from
xmodule.x_module
import
XModule
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
xblock.core
import
Scope
,
String
,
Integer
,
Boolean
,
Dict
,
List
from
capa.responsetypes
import
FormulaResponse
from
django.utils.html
import
escape
from
django.utils.html
import
escape
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -37,10 +40,15 @@ class CrowdsourceHinterFields(object):
...
@@ -37,10 +40,15 @@ class CrowdsourceHinterFields(object):
mod_queue
=
Dict
(
help
=
'A dictionary containing hints still awaiting approval'
,
scope
=
Scope
.
content
,
mod_queue
=
Dict
(
help
=
'A dictionary containing hints still awaiting approval'
,
scope
=
Scope
.
content
,
default
=
{})
default
=
{})
hint_pk
=
Integer
(
help
=
'Used to index hints.'
,
scope
=
Scope
.
content
,
default
=
0
)
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
# A list of previous hints that a student viewed.
# None if the hint was not given.
# Of the form [answer, [hint_pk_1, ...]] for each problem.
previous_answers
=
List
(
help
=
'A list of previous submissions.'
,
scope
=
Scope
.
user_state
,
default
=
[])
# 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.'
,
user_voted
=
Boolean
(
help
=
'Specifies if the user has voted on this problem or not.'
,
scope
=
Scope
.
user_state
,
default
=
False
)
scope
=
Scope
.
user_state
,
default
=
False
)
...
@@ -68,6 +76,26 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -68,6 +76,26 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
XModule
.
__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
):
def
get_html
(
self
):
"""
"""
...
@@ -98,14 +126,29 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -98,14 +126,29 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return
out
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.
of the answer.
-Lon-capa dependent.
-Lon-capa dependent.
-Assumes that the problem only has one part.
-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
):
def
handle_ajax
(
self
,
dispatch
,
data
):
"""
"""
...
@@ -124,6 +167,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -124,6 +167,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
if
out
is
None
:
if
out
is
None
:
out
=
{
'op'
:
'empty'
}
out
=
{
'op'
:
'empty'
}
elif
'error'
in
out
:
# Error in processing.
out
.
update
({
'op'
:
'error'
})
else
:
else
:
out
.
update
({
'op'
:
dispatch
})
out
.
update
({
'op'
:
dispatch
})
return
json
.
dumps
({
'contents'
:
self
.
system
.
render_template
(
'hinter_display.html'
,
out
)})
return
json
.
dumps
({
'contents'
:
self
.
system
.
render_template
(
'hinter_display.html'
,
out
)})
...
@@ -134,51 +180,67 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -134,51 +180,67 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Called by hinter javascript after a problem is graded as incorrect.
Called by hinter javascript after a problem is graded as incorrect.
Args:
Args:
`data` -- must be interpretable by
capa_
answer_to_str.
`data` -- must be interpretable by answer_to_str.
Output keys:
Output keys:
- 'best_hint' is the hint text with the most votes.
- 'hints' is a list of hint strings to show to the user.
- 'rand_hint_1' and 'rand_hint_2' are two random hints to the answer in `data`.
- 'answer' is the parsed answer that was submitted.
- '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
:
try
:
answer
=
self
.
capa_
answer_to_str
(
data
)
answer
=
self
.
answer_to_str
(
data
)
except
ValueError
:
except
(
ValueError
,
AttributeError
)
:
# Sometimes, we get an answer that's just not parsable. Do nothing.
# Sometimes, we get an answer that's just not parsable. Do nothing.
log
.
exception
(
'Answer not parsable: '
+
str
(
data
))
log
.
exception
(
'Answer not parsable: '
+
str
(
data
))
return
return
# Look for a hint to give.
if
not
self
.
validate_answer
(
answer
):
# Make a local copy of self.hints - this means we only need to do one json unpacking.
# Answer is not in the right form.
# (This is because xblocks storage makes the following command a deep copy.)
log
.
exception
(
'Answer not valid: '
+
str
(
answer
))
local_hints
=
self
.
hints
return
if
(
answer
not
in
local_hints
)
or
(
len
(
local_hints
[
answer
])
==
0
):
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.
# No hints to give. Return.
self
.
previous_answers
+=
[[
answer
,
[
None
,
None
,
None
]]]
return
return
# Get the top hint, plus two random hints.
# Get the top hint, plus two random hints.
n_hints
=
len
(
local_hints
[
answer
])
n_hints
=
len
(
matching_hints
)
best_hint_index
=
max
(
local_hints
[
answer
],
key
=
lambda
key
:
local_hints
[
answer
][
key
][
1
])
hints
=
[]
best_hint
=
local_hints
[
answer
][
best_hint_index
][
0
]
# max(dict) returns the maximum key in dict.
if
len
(
local_hints
[
answer
])
==
1
:
# The key function takes each pk, and returns the number of votes for the
rand_hint_1
=
''
# hint with that pk.
rand_hint_2
=
''
best_hint_index
=
max
(
matching_hints
,
key
=
lambda
pk
:
matching_hints
[
pk
][
1
])
self
.
previous_answers
+=
[[
answer
,
[
best_hint_index
,
None
,
None
]]]
hints
.
append
(
matching_hints
[
best_hint_index
][
0
])
elif
n_hints
==
2
:
best_hint_answer
=
matching_hints
[
best_hint_index
][
2
]
best_hint
=
local_hints
[
answer
]
.
values
()[
0
][
0
]
# The brackets surrounding the index are for backwards compatability purposes.
best_hint_index
=
local_hints
[
answer
]
.
keys
()[
0
]
# (It used to be that each answer was paired with multiple hints in a list.)
rand_hint_1
=
local_hints
[
answer
]
.
values
()[
1
][
0
]
self
.
previous_answers
+=
[[
best_hint_answer
,
[
best_hint_index
]]]
hint_index_1
=
local_hints
[
answer
]
.
keys
()[
1
]
for
_
in
xrange
(
min
(
2
,
n_hints
-
1
)):
rand_hint_2
=
''
# Keep making random hints until we hit a target, or run out.
self
.
previous_answers
+=
[[
answer
,
[
best_hint_index
,
hint_index_1
,
None
]]]
while
True
:
else
:
# random.choice randomly chooses an element from its input list.
(
hint_index_1
,
rand_hint_1
),
(
hint_index_2
,
rand_hint_2
)
=
\
# (We then unpack the item, in this case data for a hint.)
random
.
sample
(
local_hints
[
answer
]
.
items
(),
2
)
(
hint_index
,
(
rand_hint
,
_
,
hint_answer
))
=
\
rand_hint_1
=
rand_hint_1
[
0
]
random
.
choice
(
matching_hints
.
items
())
rand_hint_2
=
rand_hint_2
[
0
]
if
rand_hint
not
in
hints
:
self
.
previous_answers
+=
[[
answer
,
[
best_hint_index
,
hint_index_1
,
hint_index_2
]]]
break
hints
.
append
(
rand_hint
)
return
{
'best_hint'
:
best_hint
,
self
.
previous_answers
+=
[[
hint_answer
,
[
hint_index
]]]
'rand_hint_1'
:
rand_hint_1
,
return
{
'hints'
:
hints
,
'rand_hint_2'
:
rand_hint_2
,
'answer'
:
answer
}
'answer'
:
answer
}
def
get_feedback
(
self
,
data
):
def
get_feedback
(
self
,
data
):
...
@@ -188,38 +250,37 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -188,38 +250,37 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Args:
Args:
`data` -- not actually used. (It is assumed that the answer is correct.)
`data` -- not actually used. (It is assumed that the answer is correct.)
Output keys:
Output keys:
- 'index_to_hints' maps previous answer indices to hints that the user saw earlier.
- 'answer_to_hints': a nested dictionary.
- 'index_to_answer' maps previous answer indices to the actual answer submitted.
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.
# The student got it right.
# Did he submit at least one wrong answer?
# 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.
# No. Nothing to do here.
return
return
# Make a hint-voting interface for each wrong answer. The student will only
# 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
# be allowed to make one vote / submission, but he can choose which wrong answer
# he wants to look at.
# he wants to look at.
# index_to_hints[previous answer #] = [(hint text, hint pk), + ]
answer_to_hints
=
{}
# answer_to_hints[answer text][hint pk] -> hint text
index_to_hints
=
{}
# index_to_answer[previous answer #] = answer text
index_to_answer
=
{}
# Go through each previous answer, and populate index_to_hints and index_to_answer.
# Go through each previous answer, and populate index_to_hints and index_to_answer.
for
i
in
xrange
(
len
(
self
.
previous_answers
)):
for
i
in
xrange
(
len
(
self
.
previous_answers
)):
answer
,
hints_offered
=
self
.
previous_answers
[
i
]
answer
,
hints_offered
=
self
.
previous_answers
[
i
]
i
ndex_to_hints
[
i
]
=
[]
i
f
answer
not
in
answer_to_hints
:
index_to_answer
[
i
]
=
answer
answer_to_hints
[
answer
]
=
{}
if
answer
in
self
.
hints
:
if
answer
in
self
.
hints
:
# Go through each hint, and add to index_to_hints
# Go through each hint, and add to index_to_hints
for
hint_id
in
hints_offered
:
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
:
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
:
except
KeyError
:
# Sometimes, the hint that a user saw will have been deleted by the instructor.
# Sometimes, the hint that a user saw will have been deleted by the instructor.
continue
continue
return
{
'answer_to_hints'
:
answer_to_hints
,
return
{
'index_to_hints'
:
index_to_hints
,
'index_to_answer'
:
index_to_answer
}
'user_submissions'
:
self
.
user_submissions
}
def
tally_vote
(
self
,
data
):
def
tally_vote
(
self
,
data
):
"""
"""
...
@@ -227,31 +288,51 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -227,31 +288,51 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Args:
Args:
`data` -- expected to have the following keys:
`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
'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.
Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs.
"""
"""
if
self
.
user_voted
:
if
self
.
user_voted
:
return
{}
return
{
'error'
:
'Sorry, but you have already voted!'
}
ans_no
=
int
(
data
[
'answer'
])
ans
=
data
[
'answer'
]
hint_no
=
str
(
data
[
'hint'
])
if
not
self
.
validate_answer
(
ans
):
answer
=
self
.
previous_answers
[
ans_no
][
0
]
# 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.
# We use temp_dict because we need to do a direct write for the database to update.
temp_dict
=
self
.
hints
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
self
.
hints
=
temp_dict
# Don't let the user vote again!
# Don't let the user vote again!
self
.
user_voted
=
True
self
.
user_voted
=
True
# Return a list of how many votes each hint got.
# Return a list of how many votes each hint got.
pk_list
=
json
.
loads
(
data
[
'pk_list'
])
hint_and_votes
=
[]
hint_and_votes
=
[]
for
hint_no
in
self
.
previous_answers
[
ans_no
][
1
]:
for
answer
,
vote_pk
in
pk_list
:
if
hint_no
is
None
:
if
not
self
.
validate_answer
(
answer
):
log
.
exception
(
'In hinter tally_vote, couldn
\'
t parse {ans}'
.
format
(
ans
=
answer
))
continue
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
.
previous_answers
=
[]
self
.
user_submissions
=
[]
return
{
'hint_and_votes'
:
hint_and_votes
}
return
{
'hint_and_votes'
:
hint_and_votes
}
def
submit_hint
(
self
,
data
):
def
submit_hint
(
self
,
data
):
...
@@ -260,13 +341,17 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -260,13 +341,17 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
Args:
Args:
`data` -- expected to have the following keys:
`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
'hint': text of the new hint that the user is adding
Returns a thank-you message.
Returns a thank-you message.
"""
"""
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint
=
escape
(
data
[
'hint'
])
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.
# Only allow a student to vote or submit a hint once.
if
self
.
user_voted
:
if
self
.
user_voted
:
return
{
'message'
:
'Sorry, but you have already voted!'
}
return
{
'message'
:
'Sorry, but you have already voted!'
}
...
@@ -277,9 +362,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -277,9 +362,9 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
else
:
else
:
temp_dict
=
self
.
hints
temp_dict
=
self
.
hints
if
answer
in
temp_dict
:
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
:
else
:
temp_dict
[
answer
]
=
{
s
elf
.
hint_pk
:
[
hint
,
1
]}
temp_dict
[
answer
]
=
{
s
tr
(
self
.
hint_pk
)
:
[
hint
,
1
]}
self
.
hint_pk
+=
1
self
.
hint_pk
+=
1
if
self
.
moderate
==
'True'
:
if
self
.
moderate
==
'True'
:
self
.
mod_queue
=
temp_dict
self
.
mod_queue
=
temp_dict
...
@@ -288,10 +373,11 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
...
@@ -288,10 +373,11 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# Mark the user has having voted; reset previous_answers
# Mark the user has having voted; reset previous_answers
self
.
user_voted
=
True
self
.
user_voted
=
True
self
.
previous_answers
=
[]
self
.
previous_answers
=
[]
self
.
user_submissions
=
[]
return
{
'message'
:
'Thank you for your hint!'
}
return
{
'message'
:
'Thank you for your hint!'
}
class
CrowdsourceHinterDescriptor
(
CrowdsourceHinterFields
,
Xml
Descriptor
):
class
CrowdsourceHinterDescriptor
(
CrowdsourceHinterFields
,
Raw
Descriptor
):
module_class
=
CrowdsourceHinterModule
module_class
=
CrowdsourceHinterModule
stores_state
=
True
stores_state
=
True
...
...
common/lib/xmodule/xmodule/css/crowdsource_hinter/display.scss
View file @
b5e1d57e
...
@@ -7,52 +7,6 @@
...
@@ -7,52 +7,6 @@
background
:
rgb
(
253
,
248
,
235
);
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
{
.hint-inner-container
{
padding-left
:
15px
;
padding-left
:
15px
;
padding-right
:
15px
;
padding-right
:
15px
;
...
@@ -63,3 +17,24 @@
...
@@ -63,3 +17,24 @@
padding-top
:
0px
!
important
;
padding-top
:
0px
!
important
;
padding-bottom
:
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', ->
...
@@ -125,9 +125,10 @@ describe 'Problem', ->
expect
(
@
problem
.
bind
).
toHaveBeenCalled
()
expect
(
@
problem
.
bind
).
toHaveBeenCalled
()
describe
'check_fd'
,
->
describe
'check_fd'
,
->
xit
'should have specs written for this functionality'
,
->
xit
'should have
more
specs written for this functionality'
,
->
expect
(
false
)
expect
(
false
)
describe
'check'
,
->
describe
'check'
,
->
beforeEach
->
beforeEach
->
@
problem
=
new
Problem
(
$
(
'.xmodule_display'
))
@
problem
=
new
Problem
(
$
(
'.xmodule_display'
))
...
@@ -137,6 +138,15 @@ describe 'Problem', ->
...
@@ -137,6 +138,15 @@ describe 'Problem', ->
@
problem
.
check
()
@
problem
.
check
()
expect
(
Logger
.
log
).
toHaveBeenCalledWith
'problem_check'
,
'foo=1&bar=2'
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'
,
->
it
'submit the answer for check'
,
->
spyOn
$
,
'postWithPrefix'
spyOn
$
,
'postWithPrefix'
@
problem
.
check
()
@
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
...
@@ -19,7 +19,6 @@ class @Problem
problem_prefix
=
@
element_id
.
replace
(
/problem_/
,
''
)
problem_prefix
=
@
element_id
.
replace
(
/problem_/
,
''
)
@
inputs
=
@
$
(
"[id^=input_
#{
problem_prefix
}
_]"
)
@
inputs
=
@
$
(
"[id^=input_
#{
problem_prefix
}
_]"
)
@
$
(
'section.action input:button'
).
click
@
refreshAnswers
@
$
(
'section.action input:button'
).
click
@
refreshAnswers
@
$
(
'section.action input.check'
).
click
@
check_fd
@
$
(
'section.action input.check'
).
click
@
check_fd
@
$
(
'section.action input.reset'
).
click
@
reset
@
$
(
'section.action input.reset'
).
click
@
reset
...
@@ -247,6 +246,7 @@ class @Problem
...
@@ -247,6 +246,7 @@ class @Problem
@
updateProgress
response
@
updateProgress
response
else
else
@
gentle_alert
response
.
success
@
gentle_alert
response
.
success
Logger
.
log
'problem_graded'
,
[
@
answers
,
response
.
contents
],
@
url
if
not
abort_submission
if
not
abort_submission
$
.
ajaxWithPrefix
(
"
#{
@
url
}
/problem_check"
,
settings
)
$
.
ajaxWithPrefix
(
"
#{
@
url
}
/problem_check"
,
settings
)
...
...
common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
View file @
b5e1d57e
...
@@ -29,42 +29,73 @@ class @Hinter
...
@@ -29,42 +29,73 @@ class @Hinter
$
(
selector
,
@
el
)
$
(
selector
,
@
el
)
bind
:
=>
bind
:
=>
window
.
update_schematics
()
@
$
(
'input.vote'
).
click
@
vote
@
$
(
'input.vote'
).
click
@
vote
@
$
(
'input.submit-hint'
).
click
@
submit_hint
@
$
(
'input.submit-hint'
).
click
@
submit_hint
@
$
(
'.custom-hint'
).
click
@
clear_default_text
@
$
(
'.custom-hint'
).
click
@
clear_default_text
@
$
(
'#answer-tabs'
).
tabs
({
active
:
0
})
@
$
(
'.expand'
).
click
@
expand
@
$
(
'.expand-goodhint'
).
click
@
expand_goodhint
@
$
(
'.wizard-link'
).
click
@
wizard_link_handle
@
$
(
'.answer-choice'
).
click
@
answer_choice_handle
expand_goodhint
:
=>
expand
:
(
eventObj
)
=>
if
@
$
(
'.goodhint'
).
css
(
'display'
)
==
'none'
# Expand a hidden div.
@
$
(
'.goodhint'
).
css
(
'display'
,
'block'
)
target
=
@
$
(
'#'
+
@
$
(
eventObj
.
currentTarget
).
data
(
'target'
))
if
@
$
(
target
).
css
(
'display'
)
==
'none'
@
$
(
target
).
css
(
'display'
,
'block'
)
else
else
@
$
(
'.goodhint'
).
css
(
'display'
,
'none'
)
@
$
(
target
).
css
(
'display'
,
'none'
)
# Fix positioning errors with the bottom class.
@
set_bottom_links
()
vote
:
(
eventObj
)
=>
vote
:
(
eventObj
)
=>
# Make an ajax request with the user's vote.
target
=
@
$
(
eventObj
.
currentTarget
)
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
)
=>
$
.
postWithPrefix
"
#{
@
url
}
/vote"
,
post_json
,
(
response
)
=>
@
render
(
response
.
contents
)
@
render
(
response
.
contents
)
submit_hint
:
(
eventObj
)
=>
submit_hint
:
(
eventObj
)
=>
target
=
@
$
(
eventObj
.
currentTarget
)
# Make an ajax request with the user's new hint.
textarea_id
=
'#custom-hint-'
+
target
.
data
(
'answer'
)
textarea
=
$
(
'.custom-hint'
)
post_json
=
{
'answer'
:
target
.
data
(
'answer'
),
'hint'
:
@
$
(
textarea_id
).
val
()}
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
)
=>
$
.
postWithPrefix
"
#{
@
url
}
/submit_hint"
,
post_json
,
(
response
)
=>
@
render
(
response
.
contents
)
@
render
(
response
.
contents
)
clear_default_text
:
(
eventObj
)
=>
clear_default_text
:
(
eventObj
)
=>
# Remove placeholder text in the hint submission textbox.
target
=
@
$
(
eventObj
.
currentTarget
)
target
=
@
$
(
eventObj
.
currentTarget
)
if
target
.
data
(
'cleared'
)
==
undefined
if
target
.
data
(
'cleared'
)
==
undefined
target
.
val
(
''
)
target
.
val
(
''
)
target
.
data
(
'cleared'
,
true
)
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
)
->
render
:
(
content
)
->
if
content
if
content
# Trim leading and trailing whitespace
# Trim leading and trailing whitespace
content
=
content
.
replace
/^\s+|\s+$/g
,
""
content
=
content
.
trim
()
if
content
if
content
@
el
.
html
(
content
)
@
el
.
html
(
content
)
...
@@ -74,3 +105,37 @@ class @Hinter
...
@@ -74,3 +105,37 @@ class @Hinter
@
$
(
'#previous-answer-0'
).
css
(
'display'
,
'inline'
)
@
$
(
'#previous-answer-0'
).
css
(
'display'
,
'inline'
)
else
else
@
el
.
hide
()
@
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 @@
...
@@ -2,7 +2,7 @@
Tests the crowdsourced hinter xmodule.
Tests the crowdsourced hinter xmodule.
"""
"""
from
mock
import
Mock
from
mock
import
Mock
,
MagicMock
import
unittest
import
unittest
import
copy
import
copy
...
@@ -53,6 +53,7 @@ class CHModuleFactory(object):
...
@@ -53,6 +53,7 @@ class CHModuleFactory(object):
@staticmethod
@staticmethod
def
create
(
hints
=
None
,
def
create
(
hints
=
None
,
previous_answers
=
None
,
previous_answers
=
None
,
user_submissions
=
None
,
user_voted
=
None
,
user_voted
=
None
,
moderate
=
None
,
moderate
=
None
,
mod_queue
=
None
):
mod_queue
=
None
):
...
@@ -85,17 +86,59 @@ class CHModuleFactory(object):
...
@@ -85,17 +86,59 @@ class CHModuleFactory(object):
else
:
else
:
model_data
[
'previous_answers'
]
=
[
model_data
[
'previous_answers'
]
=
[
[
'24.0'
,
[
0
,
3
,
4
]],
[
'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
:
if
user_voted
is
not
None
:
model_data
[
'user_voted'
]
=
user_voted
model_data
[
'user_voted'
]
=
user_voted
if
moderate
is
not
None
:
if
moderate
is
not
None
:
model_data
[
'moderate'
]
=
moderate
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
()
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
)
module
=
CrowdsourceHinterModule
(
system
,
descriptor
,
model_data
)
return
module
return
module
...
@@ -146,11 +189,13 @@ class VerticalWithModulesFactory(object):
...
@@ -146,11 +189,13 @@ class VerticalWithModulesFactory(object):
@staticmethod
@staticmethod
def
next_num
():
def
next_num
():
"""Increments a global counter for naming."""
CHModuleFactory
.
num
+=
1
CHModuleFactory
.
num
+=
1
return
CHModuleFactory
.
num
return
CHModuleFactory
.
num
@staticmethod
@staticmethod
def
create
():
def
create
():
"""Make a vertical."""
model_data
=
{
'data'
:
VerticalWithModulesFactory
.
sample_problem_xml
}
model_data
=
{
'data'
:
VerticalWithModulesFactory
.
sample_problem_xml
}
system
=
get_test_system
()
system
=
get_test_system
()
descriptor
=
VerticalDescriptor
.
from_xml
(
VerticalWithModulesFactory
.
sample_problem_xml
,
system
)
descriptor
=
VerticalDescriptor
.
from_xml
(
VerticalWithModulesFactory
.
sample_problem_xml
,
system
)
...
@@ -226,6 +271,24 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -226,6 +271,24 @@ class CrowdsourceHinterTest(unittest.TestCase):
self
.
assertTrue
(
'Test numerical problem.'
in
out_html
)
self
.
assertTrue
(
'Test numerical problem.'
in
out_html
)
self
.
assertTrue
(
'Another 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
):
def
test_gethint_0hint
(
self
):
"""
"""
Someone asks for a hint, when there's no hint to give.
Someone asks for a hint, when there's no hint to give.
...
@@ -235,21 +298,36 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -235,21 +298,36 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
()
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'problem_name'
:
'26.0'
}
json_in
=
{
'problem_name'
:
'26.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
out
=
mock_module
.
get_hint
(
json_in
)
print
mock_module
.
previous_answers
self
.
assertTrue
(
out
is
None
)
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
):
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.
- The answer should not be added to previous_answers.
"""
"""
mock_module
=
CHModuleFactory
.
create
()
mock_module
=
CHModuleFactory
.
create
()
old_answers
=
copy
.
deepcopy
(
mock_module
.
previous_answers
)
old_answers
=
copy
.
deepcopy
(
mock_module
.
previous_answers
)
json_in
=
{
'problem_name'
:
'fish'
}
json_in
=
'blah'
out
=
mock_module
.
get_hint
(
json_in
)
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
(
out
is
None
)
self
.
assertTrue
(
out
is
None
)
self
.
assertTrue
(
mock_module
.
previous_answers
==
old_answers
)
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
):
def
test_gethint_1hint
(
self
):
"""
"""
Someone asks for a hint, with exactly one hint in the database.
Someone asks for a hint, with exactly one hint in the database.
...
@@ -258,7 +336,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -258,7 +336,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
()
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'problem_name'
:
'25.0'
}
json_in
=
{
'problem_name'
:
'25.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
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
):
def
test_gethint_manyhints
(
self
):
"""
"""
...
@@ -271,18 +353,18 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -271,18 +353,18 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
()
mock_module
=
CHModuleFactory
.
create
()
json_in
=
{
'problem_name'
:
'24.0'
}
json_in
=
{
'problem_name'
:
'24.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
(
out
[
'best_hint'
]
==
'Best hint'
)
self
.
assertTrue
(
'Best hint'
in
out
[
'hints'
])
self
.
assertTrue
(
'rand_hint_1'
in
out
)
self
.
assertTrue
(
len
(
out
[
'hints'
])
==
3
)
self
.
assertTrue
(
'rand_hint_2'
in
out
)
def
test_getfeedback_0wronganswers
(
self
):
def
test_getfeedback_0wronganswers
(
self
):
"""
"""
Someone has gotten the problem correct on the first try.
Someone has gotten the problem correct on the first try.
Output should be empty.
Output should be empty.
"""
"""
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[])
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[]
,
user_submissions
=
[]
)
json_in
=
{
'problem_name'
:
'42.5'
}
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
out
=
mock_module
.
get_feedback
(
json_in
)
print
out
self
.
assertTrue
(
out
is
None
)
self
.
assertTrue
(
out
is
None
)
def
test_getfeedback_1wronganswer_nohints
(
self
):
def
test_getfeedback_1wronganswer_nohints
(
self
):
...
@@ -294,9 +376,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -294,9 +376,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'26.0'
,
[
None
,
None
,
None
]]])
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'26.0'
,
[
None
,
None
,
None
]]])
json_in
=
{
'problem_name'
:
'42.5'
}
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
out
=
mock_module
.
get_feedback
(
json_in
)
print
out
[
'index_to_answer'
]
self
.
assertTrue
(
out
[
'answer_to_hints'
]
==
{
'26.0'
:
{}})
self
.
assertTrue
(
out
[
'index_to_hints'
][
0
]
==
[])
self
.
assertTrue
(
out
[
'index_to_answer'
][
0
]
==
'26.0'
)
def
test_getfeedback_1wronganswer_withhints
(
self
):
def
test_getfeedback_1wronganswer_withhints
(
self
):
"""
"""
...
@@ -307,8 +387,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -307,8 +387,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'24.0'
,
[
0
,
3
,
None
]]])
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'24.0'
,
[
0
,
3
,
None
]]])
json_in
=
{
'problem_name'
:
'42.5'
}
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
out
=
mock_module
.
get_feedback
(
json_in
)
print
out
[
'index_to_hints'
]
self
.
assertTrue
(
len
(
out
[
'answer_to_hints'
][
'24.0'
])
==
2
)
self
.
assertTrue
(
len
(
out
[
'index_to_hints'
][
0
])
==
2
)
def
test_getfeedback_missingkey
(
self
):
def
test_getfeedback_missingkey
(
self
):
"""
"""
...
@@ -319,7 +398,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -319,7 +398,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
previous_answers
=
[[
'24.0'
,
[
0
,
100
,
None
]]])
previous_answers
=
[[
'24.0'
,
[
0
,
100
,
None
]]])
json_in
=
{
'problem_name'
:
'42.5'
}
json_in
=
{
'problem_name'
:
'42.5'
}
out
=
mock_module
.
get_feedback
(
json_in
)
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
):
def
test_vote_nopermission
(
self
):
"""
"""
...
@@ -327,7 +406,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -327,7 +406,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
Should not change any vote tallies.
Should not change any vote tallies.
"""
"""
mock_module
=
CHModuleFactory
.
create
(
user_voted
=
True
)
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
)
old_hints
=
copy
.
deepcopy
(
mock_module
.
hints
)
mock_module
.
tally_vote
(
json_in
)
mock_module
.
tally_vote
(
json_in
)
self
.
assertTrue
(
mock_module
.
hints
==
old_hints
)
self
.
assertTrue
(
mock_module
.
hints
==
old_hints
)
...
@@ -339,19 +418,56 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -339,19 +418,56 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
"""
mock_module
=
CHModuleFactory
.
create
(
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'24.0'
,
[
0
,
3
,
None
]]])
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
)
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'
][
'0'
][
1
]
==
40
)
self
.
assertTrue
(
mock_module
.
hints
[
'24.0'
][
'3'
][
1
]
==
31
)
self
.
assertTrue
(
mock_module
.
hints
[
'24.0'
][
'3'
][
1
]
==
31
)
self
.
assertTrue
([
'Best hint'
,
40
]
in
dict_out
[
'hint_and_votes'
])
self
.
assertTrue
([
'Best hint'
,
40
]
in
dict_out
[
'hint_and_votes'
])
self
.
assertTrue
([
'Another hint'
,
31
]
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
):
def
test_submithint_nopermission
(
self
):
"""
"""
A user tries to submit a hint, but he has already voted.
A user tries to submit a hint, but he has already voted.
"""
"""
mock_module
=
CHModuleFactory
.
create
(
user_voted
=
True
)
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
print
mock_module
.
user_voted
mock_module
.
submit_hint
(
json_in
)
mock_module
.
submit_hint
(
json_in
)
print
mock_module
.
hints
print
mock_module
.
hints
...
@@ -363,7 +479,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -363,7 +479,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
exist yet.
exist yet.
"""
"""
mock_module
=
CHModuleFactory
.
create
()
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
)
mock_module
.
submit_hint
(
json_in
)
self
.
assertTrue
(
'29.0'
in
mock_module
.
hints
)
self
.
assertTrue
(
'29.0'
in
mock_module
.
hints
)
...
@@ -373,13 +489,12 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -373,13 +489,12 @@ class CrowdsourceHinterTest(unittest.TestCase):
already.
already.
"""
"""
mock_module
=
CHModuleFactory
.
create
(
previous_answers
=
[[
'25.0'
,
[
1
,
None
,
None
]]])
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
)
mock_module
.
submit_hint
(
json_in
)
# Make a hint request.
# Make a hint request.
json_in
=
{
'problem name'
:
'25.0'
}
json_in
=
{
'problem name'
:
'25.0'
}
out
=
mock_module
.
get_hint
(
json_in
)
out
=
mock_module
.
get_hint
(
json_in
)
self
.
assertTrue
((
out
[
'best_hint'
]
==
'This is a new hint.'
)
self
.
assertTrue
(
'This is a new hint.'
in
out
[
'hints'
])
or
(
out
[
'rand_hint_1'
]
==
'This is a new hint.'
))
def
test_submithint_moderate
(
self
):
def
test_submithint_moderate
(
self
):
"""
"""
...
@@ -388,7 +503,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -388,7 +503,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
dict.
dict.
"""
"""
mock_module
=
CHModuleFactory
.
create
(
moderate
=
'True'
)
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
)
mock_module
.
submit_hint
(
json_in
)
self
.
assertTrue
(
'29.0'
not
in
mock_module
.
hints
)
self
.
assertTrue
(
'29.0'
not
in
mock_module
.
hints
)
self
.
assertTrue
(
'29.0'
in
mock_module
.
mod_queue
)
self
.
assertTrue
(
'29.0'
in
mock_module
.
mod_queue
)
...
@@ -398,10 +513,20 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -398,10 +513,20 @@ class CrowdsourceHinterTest(unittest.TestCase):
Make sure that hints are being html-escaped.
Make sure that hints are being html-escaped.
"""
"""
mock_module
=
CHModuleFactory
.
create
()
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
)
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
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
):
def
test_template_gethint
(
self
):
"""
"""
...
@@ -409,7 +534,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
...
@@ -409,7 +534,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
"""
mock_module
=
CHModuleFactory
.
create
()
mock_module
=
CHModuleFactory
.
create
()
def
fake_get_hint
(
get
):
def
fake_get_hint
(
_
):
"""
"""
Creates a rendering dictionary, with which we can test
Creates a rendering dictionary, with which we can test
the templates.
the templates.
...
...
common/templates/hinter_display.html
View file @
b5e1d57e
...
@@ -3,65 +3,100 @@
...
@@ -3,65 +3,100 @@
<
%
def
name=
"get_hint()"
>
<
%
def
name=
"get_hint()"
>
% if
best_hint != ''
:
% if
len(hints) > 0
:
<h4>
Hints from students who made similar mistakes:
</h4>
<h4>
Hints from students who made similar mistakes:
</h4>
<ul>
<ul>
<li>
${best_hint}
</li>
% for hint in hints:
% endif
<li>
${hint}
</li>
% if rand_hint_1 != '':
% endfor
<li>
${rand_hint_1}
</li>
% endif
% if rand_hint_2 != '':
<li>
${rand_hint_2}
</li>
% endif
</ul>
</ul>
% endif
</
%
def>
</
%
def>
<
%
def
name=
"get_feedback()"
>
<
%
def
name=
"get_feedback()"
>
<p><em>
Participation in the hinting system is strictly optional, and will not influence your grade.
</em></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>
<p>
Help your classmates by writing hints for this problem. Start by picking one of your previous incorrect answers from below:
<em>
Optional.
</em>
Help us improve our hints! Which hint was most helpful to you?
</p>
</p>
<div
id=
"answer-tabs"
>
<div
id=
"pk-list"
data-pk-list=
'${json_pk_list}'
style=
"display:none"
>
</div>
<ul>
% for index, answer in index_to_answer.items():
<li><a
href=
"#previous-answer-${index}"
>
${answer}
</a></li>
% endfor
</ul>
% for index, answer in index_to_answer.items():
% for answer, pk_dict in answer_to_hints.items():
<div
class =
"previous-answer"
id=
"previous-answer-${index}"
>
% for hint_pk, hint_text in pk_dict.items():
<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>
<p>
<input
class=
"vote"
data-answer=
"${index}"
data-hintno=
"${hint_pk}"
type=
"button"
value=
"Vote"
/
>
<input
class=
"vote"
data-answer=
"${answer}"
data-hintno=
"${hint_pk}"
type=
"button"
value=
"Vote"
>
${hint_text}
${hint_text}
</p>
</p>
% endfor
% endfor
% endfor
<p>
<p>
Don't like any of the hints above? You can also submit your own.
Don't like any of the hints above?
<a
class=
"wizard-link"
dest=
"p2"
href=
"javascript: void(0);"
>
Write your own!
</a></p>
</div>
<div
class=
"wizard-view"
id=
"p2"
>
% if hints_exist:
<p>
Choose the incorrect answer for which you want to write a hint:
</p>
</p>
% endif
% else:
<p>
<p>
What hint would you give a student who made the same mistake you did? Please don't give away the answer.
<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>
</p>
<textarea
cols=
"50"
class=
"custom-hint"
id=
"custom-hint-${index}"
>
% endif
What would you say to help someone who got this wrong answer?
% for answer in user_submissions:
(Don't give away the answer, please.)
<a
class=
"answer-choice"
href=
"javascript: void(0)"
value=
"${answer}"
>
${answer}
</a><br
/>
</textarea>
<br/><br/>
<input
class=
"submit-hint"
data-answer=
"${index}"
type=
"button"
value=
"submit"
>
</div></div>
% endfor
% endfor
% if hints_exist:
<p
class=
"bottom"
>
<a
href=
"javascript: void(0);"
class=
"wizard-link"
dest=
"p1"
>
Back
</a>
</p>
% endif
</div>
</div>
<p>
Read about
<a
class=
"expand-goodhint"
href=
"javascript:void(0);"
>
what makes a good hint
</a>
.
</p>
<div
class=
"wizard-view"
id=
"p3"
>
<div
class=
"goodhint"
style=
"display:none"
>
<p>
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>
<h4>
What makes a good hint?
</h4>
<p>
It depends on the type of problem you ran into. For stupid errors --
<p>
It depends on the type of problem you ran into. For stupid errors --
...
@@ -90,7 +125,15 @@ What would you say to help someone who got this wrong answer?
...
@@ -90,7 +125,15 @@ What would you say to help someone who got this wrong answer?
<p>
<p>
<a
href=
"http://www.apa.org/education/k12/misconceptions.aspx?item=2"
target=
"_blank"
>
Learn even more
</a>
<a
href=
"http://www.apa.org/education/k12/misconceptions.aspx?item=2"
target=
"_blank"
>
Learn even more
</a>
</p>
</div>
</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>
</
%
def>
</
%
def>
...
@@ -124,6 +167,10 @@ What would you say to help someone who got this wrong answer?
...
@@ -124,6 +167,10 @@ What would you say to help someone who got this wrong answer?
${simple_message()}
${simple_message()}
% endif
% endif
% if op == "error":
${error}
% endif
% if op == "vote":
% if op == "vote":
${show_votes()}
${show_votes()}
% endif
% endif
...
...
lms/djangoapps/instructor/hint_manager.py
View file @
b5e1d57e
"""
"""
Views for hint management.
Views for hint management.
Along with the crowdsource_hinter xmodule, this code is still
Get to these views through courseurl/hint_manager.
experimental, and should not be used in new courses, yet.
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
import
json
...
@@ -15,12 +17,17 @@ from mitxmako.shortcuts import render_to_response, render_to_string
...
@@ -15,12 +17,17 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from
courseware.courses
import
get_course_with_access
from
courseware.courses
import
get_course_with_access
from
courseware.models
import
XModuleContentField
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
import
Location
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
@ensure_csrf_cookie
@ensure_csrf_cookie
def
hint_manager
(
request
,
course_id
):
def
hint_manager
(
request
,
course_id
):
"""
The URL landing function for all calls to the hint manager, both POST and GET.
"""
try
:
try
:
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
get_course_with_access
(
request
.
user
,
course_id
,
'staff'
,
depth
=
None
)
except
Http404
:
except
Http404
:
...
@@ -28,24 +35,29 @@ def hint_manager(request, course_id):
...
@@ -28,24 +35,29 @@ def hint_manager(request, course_id):
return
HttpResponse
(
out
)
return
HttpResponse
(
out
)
if
request
.
method
==
'GET'
:
if
request
.
method
==
'GET'
:
out
=
get_hints
(
request
,
course_id
,
'mod_queue'
)
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'
]
field
=
request
.
POST
[
'field'
]
if
not
(
field
==
'mod_queue'
or
field
==
'hints'
):
if
not
(
field
==
'mod_queue'
or
field
==
'hints'
):
# Invalid field. (Don't let users continue - they may overwrite other db's)
# Invalid field. (Don't let users continue - they may overwrite other db's)
out
=
'Error in hint manager - an invalid field was accessed.'
out
=
'Error in hint manager - an invalid field was accessed.'
return
HttpResponse
(
out
)
return
HttpResponse
(
out
)
if
request
.
POST
[
'op'
]
==
'delete hints'
:
switch_dict
=
{
delete_hints
(
request
,
course_id
,
field
)
'delete hints'
:
delete_hints
,
if
request
.
POST
[
'op'
]
==
'switch fields'
:
'switch fields'
:
lambda
*
args
:
None
,
# Takes any number of arguments, returns None.
pass
'change votes'
:
change_votes
,
if
request
.
POST
[
'op'
]
==
'change votes'
:
'add hint'
:
add_hint
,
change_votes
(
request
,
course_id
,
field
)
'approve'
:
approve
,
if
request
.
POST
[
'op'
]
==
'add hint'
:
}
add_hint
(
request
,
course_id
,
field
)
if
request
.
POST
[
'op'
]
==
'approve'
:
# Do the operation requested, and collect any error messages.
approve
(
request
,
course_id
,
field
)
error_text
=
switch_dict
[
request
.
POST
[
'op'
]](
request
,
course_id
,
field
)
rendered_html
=
render_to_string
(
'courseware/hint_manager_inner.html'
,
get_hints
(
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
}))
return
HttpResponse
(
json
.
dumps
({
'success'
:
True
,
'contents'
:
rendered_html
}))
...
@@ -165,7 +177,13 @@ def change_votes(request, course_id, field):
...
@@ -165,7 +177,13 @@ def change_votes(request, course_id, field):
Updates the number of votes.
Updates the number of votes.
The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples.
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
:
for
key
in
request
.
POST
:
...
@@ -193,6 +211,18 @@ def add_hint(request, course_id, field):
...
@@ -193,6 +211,18 @@ def add_hint(request, course_id, field):
problem_id
=
request
.
POST
[
'problem'
]
problem_id
=
request
.
POST
[
'problem'
]
answer
=
request
.
POST
[
'answer'
]
answer
=
request
.
POST
[
'answer'
]
hint_text
=
request
.
POST
[
'hint'
]
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
)
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
)
hint_pk_entry
=
XModuleContentField
.
objects
.
get
(
field_name
=
'hint_pk'
,
definition_id
=
problem_id
)
...
@@ -214,6 +244,8 @@ def approve(request, course_id, field):
...
@@ -214,6 +244,8 @@ def approve(request, course_id, field):
hint list. POST:
hint list. POST:
op, field
op, field
(some number) -> [problem, answer, pk]
(some number) -> [problem, answer, pk]
The numbered fields are analogous to those in `delete_hints` and `change_votes`.
"""
"""
for
key
in
request
.
POST
:
for
key
in
request
.
POST
:
...
...
lms/djangoapps/instructor/tests/test_hint_manager.py
View file @
b5e1d57e
...
@@ -2,6 +2,7 @@ import json
...
@@ -2,6 +2,7 @@ import json
from
django.test.client
import
Client
,
RequestFactory
from
django.test.client
import
Client
,
RequestFactory
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
mock
import
patch
,
MagicMock
from
courseware.models
import
XModuleContentField
from
courseware.models
import
XModuleContentField
from
courseware.tests.factories
import
ContentFactory
from
courseware.tests.factories
import
ContentFactory
...
@@ -137,16 +138,45 @@ class HintManagerTest(ModuleStoreTestCase):
...
@@ -137,16 +138,45 @@ class HintManagerTest(ModuleStoreTestCase):
"""
"""
Check that instructors can add new hints.
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
()
request
=
RequestFactory
()
post
=
request
.
post
(
self
.
url
,
{
'field'
:
'mod_queue'
,
post
=
request
.
post
(
self
.
url
,
{
'field'
:
'mod_queue'
,
'op'
:
'add hint'
,
'op'
:
'add hint'
,
'problem'
:
self
.
problem_id
,
'problem'
:
self
.
problem_id
,
'answer'
:
'3.14'
,
'answer'
:
'3.14'
,
'hint'
:
'This is a new hint.'
})
'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'
)
view
.
add_hint
(
post
,
self
.
course_id
,
'mod_queue'
)
problem_hints
=
XModuleContentField
.
objects
.
get
(
field_name
=
'mod_queue'
,
definition_id
=
self
.
problem_id
)
.
value
problem_hints
=
XModuleContentField
.
objects
.
get
(
field_name
=
'mod_queue'
,
definition_id
=
self
.
problem_id
)
.
value
self
.
assertTrue
(
'3.14'
in
json
.
loads
(
problem_hints
))
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
):
def
test_approve
(
self
):
"""
"""
Check that instructors can approve hints. (Move them
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"
/>
<
%
inherit
file=
"/main.html"
/>
<
%
namespace
name=
'static'
file=
'/static_content.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"
>
<
%
block
name=
"headextra"
>
...
...
lms/templates/
courseware
/hint_manager_inner.html
→
lms/templates/
instructor
/hint_manager_inner.html
View file @
b5e1d57e
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
<h1>
${field_label}
</h1>
<h1>
${field_label}
</h1>
Switch to
<a
id=
"switch-fields"
other-field=
"${other_field}"
>
${other_field_label}
</a>
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:
% for definition_id in all_hints:
<h2>
Problem: ${id_to_name[definition_id]}
</h2>
<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
...
@@ -36,6 +37,7 @@ Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label
<br
/>
<br
/>
% endfor
% endfor
<p
style=
"color:red"
>
${error}
</p>
<button
id=
"hint-delete"
>
Delete selected
</button>
<button
id=
"update-votes"
>
Update votes
</button>
<button
id=
"hint-delete"
>
Delete selected
</button>
<button
id=
"update-votes"
>
Update votes
</button>
% if field == 'mod_queue':
% 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