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
b1d91fad
Commit
b1d91fad
authored
Nov 14, 2013
by
Sarina Canelake
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1663 from edx/sarina/persist-student-answers-on-error
Sarina/persist student answers on error
parents
dffbb8a3
9508b3f9
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
70 additions
and
45 deletions
+70
-45
common/lib/capa/capa/responsetypes.py
+48
-29
common/lib/xmodule/xmodule/capa_module.py
+22
-16
No files found.
common/lib/capa/capa/responsetypes.py
View file @
b1d91fad
...
...
@@ -655,6 +655,7 @@ class ChoiceResponse(LoncapaResponse):
response_tag
=
'choiceresponse'
max_inputfields
=
1
allowed_inputfields
=
[
'checkboxgroup'
,
'radiogroup'
]
correct_choices
=
None
def
setup_response
(
self
):
...
...
@@ -706,6 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
response_tag
=
'multiplechoiceresponse'
max_inputfields
=
1
allowed_inputfields
=
[
'choicegroup'
]
correct_choices
=
None
def
setup_response
(
self
):
# call secondary setup for MultipleChoice questions, to set name
...
...
@@ -791,6 +793,7 @@ class OptionResponse(LoncapaResponse):
response_tag
=
'optionresponse'
hint_tag
=
'optionhint'
allowed_inputfields
=
[
'optioninput'
]
answer_fields
=
None
def
setup_response
(
self
):
self
.
answer_fields
=
self
.
inputfields
...
...
@@ -949,6 +952,7 @@ class StringResponse(LoncapaResponse):
allowed_inputfields
=
[
'textline'
]
required_attributes
=
[
'answer'
]
max_inputfields
=
1
correct_answer
=
None
def
setup_response
(
self
):
self
.
correct_answer
=
contextualize_text
(
...
...
@@ -974,7 +978,7 @@ class StringResponse(LoncapaResponse):
hxml
.
get
(
'answer'
),
self
.
context
)
.
strip
()
if
self
.
check_string
(
correct_answer
,
given
):
hints_to_show
.
append
(
name
)
log
.
debug
(
'hints_to_show =
%
s'
%
hints_to_show
)
log
.
debug
(
'hints_to_show =
%
s'
,
hints_to_show
)
return
hints_to_show
def
get_answers
(
self
):
...
...
@@ -996,6 +1000,8 @@ class CustomResponse(LoncapaResponse):
'drag_and_drop_input'
,
'editamoleculeinput'
,
'designprotein2dinput'
,
'editageneinput'
,
'annotationinput'
,
'jsinput'
,
'formulaequationinput'
]
code
=
None
expect
=
None
def
setup_response
(
self
):
xml
=
self
.
xml
...
...
@@ -1004,7 +1010,7 @@ class CustomResponse(LoncapaResponse):
# that
self
.
expect
=
xml
.
get
(
'expect'
)
or
xml
.
get
(
'answer'
)
log
.
debug
(
'answer_ids=
%
s'
%
self
.
answer_ids
)
log
.
debug
(
'answer_ids=
%
s'
,
self
.
answer_ids
)
# the <answer>...</answer> stanza should be local to the current <customresponse>.
# So try looking there first.
...
...
@@ -1020,7 +1026,7 @@ class CustomResponse(LoncapaResponse):
# <script>...</script> stanza instead
cfn
=
xml
.
get
(
'cfn'
)
if
cfn
:
log
.
debug
(
"cfn =
%
s"
%
cfn
)
log
.
debug
(
"cfn =
%
s"
,
cfn
)
# This is a bit twisty. We used to grab the cfn function from
# the context, but now that we sandbox Python execution, we
...
...
@@ -1055,7 +1061,7 @@ class CustomResponse(LoncapaResponse):
if
not
self
.
code
:
if
answer
is
None
:
log
.
error
(
"[courseware.capa.responsetypes.customresponse] missing"
" code checking script! id=
%
s"
%
self
.
id
)
" code checking script! id=
%
s"
,
self
.
id
)
self
.
code
=
''
else
:
answer_src
=
answer
.
get
(
'src'
)
...
...
@@ -1071,7 +1077,7 @@ class CustomResponse(LoncapaResponse):
of each key removed (the string before the first "_").
'''
log
.
debug
(
'
%
s: student_answers=
%
s'
%
(
unicode
(
self
),
student_answers
)
)
log
.
debug
(
'
%
s: student_answers=
%
s'
,
unicode
(
self
),
student_answers
)
# ordered list of answer id's
idset
=
sorted
(
self
.
answer_ids
)
...
...
@@ -1182,10 +1188,10 @@ class CustomResponse(LoncapaResponse):
answer_given
=
submission
[
0
]
if
(
len
(
idset
)
==
1
)
else
submission
kwnames
=
self
.
xml
.
get
(
"cfn_extra_args"
,
""
)
.
split
()
kwargs
=
{
n
:
self
.
context
.
get
(
n
)
for
n
in
kwnames
}
log
.
debug
(
" submission =
%
s"
%
submission
)
log
.
debug
(
" submission =
%
s"
,
submission
)
try
:
ret
=
fn
(
self
.
expect
,
answer_given
,
**
kwargs
)
except
Exception
as
err
:
except
Exception
as
err
:
# pylint: disable=broad-except
self
.
_handle_exec_exception
(
err
)
log
.
debug
(
"[courseware.capa.responsetypes.customresponse.get_score] ret =
%
s"
,
...
...
@@ -1340,22 +1346,21 @@ class SymbolicResponse(CustomResponse):
debug
=
self
.
context
.
get
(
'debug'
),
)
except
Exception
as
err
:
log
.
error
(
"oops in symbolicresponse (cfn) error
%
s"
%
err
)
log
.
error
(
"oops in symbolicresponse (cfn) error
%
s"
,
err
)
log
.
error
(
traceback
.
format_exc
())
raise
Exception
(
"oops in symbolicresponse (cfn) error
%
s"
%
err
)
raise
Exception
(
"oops in symbolicresponse (cfn) error
%
s"
,
err
)
self
.
context
[
'messages'
][
0
]
=
self
.
clean_message_html
(
ret
[
'msg'
])
self
.
context
[
'correct'
]
=
[
'correct'
if
ret
[
'ok'
]
else
'incorrect'
]
*
len
(
idset
)
#-----------------------------------------------------------------------------
"""
valid: Flag indicating valid score_msg format (Boolean)
correct: Correctness of submission (Boolean)
score: Points to be assigned (numeric, can be float)
msg: Message from grader to display to student (string)
"""
ScoreMessage
=
namedtuple
(
'ScoreMessage'
,
[
'valid'
,
'correct'
,
'points'
,
'msg'
])
## ScoreMessage named tuple ##
## valid: Flag indicating valid score_msg format (Boolean)
## correct: Correctness of submission (Boolean)
## score: Points to be assigned (numeric, can be float)
## msg: Message from grader to display to student (string)
ScoreMessage
=
namedtuple
(
'ScoreMessage'
,
[
'valid'
,
'correct'
,
'points'
,
'msg'
])
# pylint: disable=invalid-name
class
CodeResponse
(
LoncapaResponse
):
...
...
@@ -1377,6 +1382,11 @@ class CodeResponse(LoncapaResponse):
response_tag
=
'coderesponse'
allowed_inputfields
=
[
'textbox'
,
'filesubmission'
,
'matlabinput'
]
max_inputfields
=
1
payload
=
None
initial_display
=
None
url
=
None
answer
=
None
queue_name
=
None
def
setup_response
(
self
):
'''
...
...
@@ -1427,8 +1437,8 @@ class CodeResponse(LoncapaResponse):
except
Exception
as
err
:
log
.
error
(
'Error in CodeResponse
%
s: cannot get student answer for
%
s;'
' student_answers=
%
s'
%
(
err
,
self
.
answer_id
,
convert_files_to_filenames
(
student_answers
)
)
' student_answers=
%
s'
,
err
,
self
.
answer_id
,
convert_files_to_filenames
(
student_answers
)
)
raise
Exception
(
err
)
...
...
@@ -1511,9 +1521,8 @@ class CodeResponse(LoncapaResponse):
return
cmap
def
update_score
(
self
,
score_msg
,
oldcmap
,
queuekey
):
(
valid_score_msg
,
correct
,
points
,
msg
)
=
self
.
_parse_score_msg
(
score_msg
)
"""Updates the user's score based on the returned message from the grader."""
(
valid_score_msg
,
correct
,
points
,
msg
)
=
self
.
_parse_score_msg
(
score_msg
)
if
not
valid_score_msg
:
oldcmap
.
set
(
self
.
answer_id
,
msg
=
'Invalid grader reply. Please contact the course staff.'
)
...
...
@@ -1536,8 +1545,11 @@ class CodeResponse(LoncapaResponse):
self
.
answer_id
,
npoints
=
points
,
correctness
=
correctness
,
msg
=
msg
.
replace
(
' '
,
' '
),
queuestate
=
None
)
else
:
log
.
debug
(
'CodeResponse: queuekey
%
s does not match for answer_id=
%
s.'
%
(
queuekey
,
self
.
answer_id
))
log
.
debug
(
'CodeResponse: queuekey
%
s does not match for answer_id=
%
s.'
,
queuekey
,
self
.
answer_id
)
return
oldcmap
...
...
@@ -1546,6 +1558,10 @@ class CodeResponse(LoncapaResponse):
return
{
self
.
answer_id
:
anshtml
}
def
get_initial_display
(
self
):
"""
The course author can specify an initial display
to be displayed the code response box.
"""
return
{
self
.
answer_id
:
self
.
initial_display
}
def
_parse_score_msg
(
self
,
score_msg
):
...
...
@@ -1566,11 +1582,11 @@ class CodeResponse(LoncapaResponse):
score_result
=
json
.
loads
(
score_msg
)
except
(
TypeError
,
ValueError
):
log
.
error
(
"External grader message should be a JSON-serialized dict."
" Received score_msg =
%
s"
%
score_msg
)
" Received score_msg =
%
s"
,
score_msg
)
return
fail
if
not
isinstance
(
score_result
,
dict
):
log
.
error
(
"External grader message should be a JSON-serialized dict."
" Received score_result =
%
s"
%
score_result
)
" Received score_result =
%
s"
,
score_result
)
return
fail
for
tag
in
[
'correct'
,
'score'
,
'msg'
]:
if
tag
not
in
score_result
:
...
...
@@ -1585,9 +1601,9 @@ class CodeResponse(LoncapaResponse):
msg
=
score_result
[
'msg'
]
try
:
etree
.
fromstring
(
msg
)
except
etree
.
XMLSyntaxError
as
err
:
except
etree
.
XMLSyntaxError
as
_
err
:
log
.
error
(
"Unable to parse external grader message as valid"
" XML: score_msg['msg']=
%
s"
%
msg
)
" XML: score_msg['msg']=
%
s"
,
msg
)
return
fail
return
(
True
,
score_result
[
'correct'
],
score_result
[
'score'
],
msg
)
...
...
@@ -1805,7 +1821,10 @@ class FormulaResponse(LoncapaResponse):
def
get_score
(
self
,
student_answers
):
given
=
student_answers
[
self
.
answer_id
]
correctness
=
self
.
check_formula
(
self
.
correct_answer
,
given
,
self
.
samples
)
self
.
correct_answer
,
given
,
self
.
samples
)
return
CorrectMap
(
self
.
answer_id
,
correctness
)
def
tupleize_answers
(
self
,
answer
,
var_dict_list
):
...
...
common/lib/xmodule/xmodule/capa_module.py
View file @
b1d91fad
"""Implements basics of Capa, including class CapaModule."""
import
cgi
import
datetime
import
hashlib
...
...
@@ -40,11 +41,11 @@ def randomization_bin(seed, problem_id):
interesting. To avoid having sets of students that always get the same problems,
we'll combine the system's per-student seed with the problem id in picking the bin.
"""
h
=
hashlib
.
sha1
()
h
.
update
(
str
(
seed
))
h
.
update
(
str
(
problem_id
))
r_has
h
=
hashlib
.
sha1
()
r_has
h
.
update
(
str
(
seed
))
r_has
h
.
update
(
str
(
problem_id
))
# get the first few digits of the hash, convert to an int, then mod.
return
int
(
h
.
hexdigest
()[:
7
],
16
)
%
NUM_RANDOMIZATION_BINS
return
int
(
r_has
h
.
hexdigest
()[:
7
],
16
)
%
NUM_RANDOMIZATION_BINS
class
Randomization
(
String
):
...
...
@@ -220,7 +221,7 @@ class CapaModule(CapaFields, XModule):
if
self
.
seed
is
None
:
self
.
seed
=
self
.
lcp
.
seed
except
Exception
as
err
:
except
Exception
as
err
:
# pylint: disable=broad-except
msg
=
u'cannot create LoncapaProblem {loc}: {err}'
.
format
(
loc
=
self
.
location
.
url
(),
err
=
err
)
# TODO (vshnayder): do modules need error handlers too?
...
...
@@ -318,9 +319,9 @@ class CapaModule(CapaFields, XModule):
"""
For now, just return score / max_score
"""
d
=
self
.
get_score
()
score
=
d
[
'score'
]
total
=
d
[
'total'
]
score_dict
=
self
.
get_score
()
score
=
score_dict
[
'score'
]
total
=
score_dict
[
'total'
]
if
total
>
0
:
if
self
.
weight
is
not
None
:
...
...
@@ -525,7 +526,7 @@ class CapaModule(CapaFields, XModule):
# If we cannot construct the problem HTML,
# then generate an error message instead.
except
Exception
as
err
:
except
Exception
as
err
:
# pylint: disable=broad-except
html
=
self
.
handle_problem_html_error
(
err
)
# The convention is to pass the name of the check button
...
...
@@ -610,11 +611,11 @@ class CapaModule(CapaFields, XModule):
result
=
handlers
[
dispatch
](
data
)
except
NotFoundError
as
err
:
_
,
_
,
traceback_obj
=
sys
.
exc_info
()
_
,
_
,
traceback_obj
=
sys
.
exc_info
()
# pylint: disable=redefined-outer-name
raise
ProcessingError
,
(
not_found_error_message
,
err
),
traceback_obj
except
Exception
as
err
:
_
,
_
,
traceback_obj
=
sys
.
exc_info
()
_
,
_
,
traceback_obj
=
sys
.
exc_info
()
# pylint: disable=redefined-outer-name
raise
ProcessingError
,
(
generic_error_message
,
err
),
traceback_obj
after
=
self
.
get_progress
()
...
...
@@ -668,8 +669,8 @@ class CapaModule(CapaFields, XModule):
"""
True iff full points
"""
d
=
self
.
get_score
()
return
d
[
'score'
]
==
d
[
'total'
]
score_dict
=
self
.
get_score
()
return
score_dict
[
'score'
]
==
score_dict
[
'total'
]
def
answer_available
(
self
):
"""
...
...
@@ -757,7 +758,7 @@ class CapaModule(CapaFields, XModule):
self
.
set_state_from_lcp
()
return
response
def
get_answer
(
self
,
data
):
def
get_answer
(
self
,
_
data
):
"""
For the "show answer" button.
...
...
@@ -797,7 +798,6 @@ class CapaModule(CapaFields, XModule):
"""
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
@staticmethod
def
make_dict_of_responses
(
data
):
"""
...
...
@@ -840,7 +840,7 @@ class CapaModule(CapaFields, XModule):
# We only want to consider each key a single time, so we use set(data.keys())
for
key
in
set
(
data
.
keys
()):
# e.g. input_resistor_1 ==> resistor_1
_
,
_
,
name
=
key
.
partition
(
'_'
)
_
,
_
,
name
=
key
.
partition
(
'_'
)
# pylint: disable=redefined-outer-name
# If key has no underscores, then partition
# will return (key, '', '')
...
...
@@ -941,6 +941,9 @@ class CapaModule(CapaFields, XModule):
log
.
warning
(
"StudentInputError in capa_module:problem_check"
,
exc_info
=
True
)
# Save the user's state before failing
self
.
set_state_from_lcp
()
# If the user is a staff member, include
# the full exception, including traceback,
# in the response
...
...
@@ -955,6 +958,9 @@ class CapaModule(CapaFields, XModule):
return
{
'success'
:
msg
}
except
Exception
as
err
:
# Save the user's state before failing
self
.
set_state_from_lcp
()
if
self
.
system
.
DEBUG
:
msg
=
u"Error checking problem: {}"
.
format
(
err
.
message
)
msg
+=
u'
\n
Traceback:
\n
{}'
.
format
(
traceback
.
format_exc
())
...
...
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