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
f0e1b477
Commit
f0e1b477
authored
Mar 25, 2013
by
Victor Shnayder
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1703 from MITx/feature/diana/matlab-input
Matlab Input Type
parents
a91f1278
4bda05d9
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
789 additions
and
236 deletions
+789
-236
common/lib/capa/capa/capa_problem.py
+45
-25
common/lib/capa/capa/inputtypes.py
+233
-62
common/lib/capa/capa/responsetypes.py
+224
-136
common/lib/capa/capa/templates/matlabinput.html
+117
-0
common/lib/capa/capa/tests/__init__.py
+7
-2
common/lib/capa/capa/tests/test_inputtypes.py
+93
-0
common/lib/xmodule/xmodule/capa_module.py
+42
-1
common/lib/xmodule/xmodule/js/src/capa/display.coffee
+6
-0
common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+2
-2
common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+4
-1
lms/djangoapps/courseware/module_render.py
+16
-7
No files found.
common/lib/capa/capa/capa_problem.py
View file @
f0e1b477
...
...
@@ -16,7 +16,6 @@ This is used by capa_module.
from
__future__
import
division
from
datetime
import
datetime
import
json
import
logging
import
math
import
numpy
...
...
@@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape
from
copy
import
deepcopy
import
chem
import
chem.chemcalc
import
chem.chemtools
import
chem.miller
import
verifiers
import
verifiers.draganddrop
...
...
@@ -70,9 +67,6 @@ global_context = {'random': random,
'scipy'
:
scipy
,
'calc'
:
calc
,
'eia'
:
eia
,
'chemcalc'
:
chem
.
chemcalc
,
'chemtools'
:
chem
.
chemtools
,
'miller'
:
chem
.
miller
,
'draganddrop'
:
verifiers
.
draganddrop
}
# These should be removed from HTML output, including all subelements
...
...
@@ -97,8 +91,13 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- state (dict): student state
- seed (int): random number generator seed (int)
- seed (int): random number generator seed (int)
- state (dict): containing the following keys:
- 'seed' - (int) random number generator seed
- 'student_answers' - (dict) maps input id to the stored answer for that input
- 'correct_map' (CorrectMap) a map of each input to their 'correctness'
- 'done' - (bool) indicates whether or not this problem is considered done
- 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
...
...
@@ -110,21 +109,23 @@ class LoncapaProblem(object):
self
.
system
=
system
if
self
.
system
is
None
:
raise
Exception
()
self
.
seed
=
seed
if
state
:
if
'seed'
in
state
:
self
.
seed
=
state
[
'seed'
]
if
'student_answers'
in
state
:
self
.
student_answers
=
state
[
'student_answers'
]
if
'correct_map'
in
state
:
self
.
correct_map
.
set_dict
(
state
[
'correct_map'
])
if
'done'
in
state
:
self
.
done
=
state
[
'done'
]
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if
not
self
.
seed
:
self
.
seed
=
struct
.
unpack
(
'i'
,
os
.
urandom
(
4
))[
0
]
state
=
state
if
state
else
{}
# Set seed according to the following priority:
# 1. Contained in problem's state
# 2. Passed into capa_problem via constructor
# 3. Assign from the OS's random number generator
self
.
seed
=
state
.
get
(
'seed'
,
seed
)
if
self
.
seed
is
None
:
self
.
seed
=
struct
.
unpack
(
'i'
,
os
.
urandom
(
4
))
self
.
student_answers
=
state
.
get
(
'student_answers'
,
{})
if
'correct_map'
in
state
:
self
.
correct_map
.
set_dict
(
state
[
'correct_map'
])
self
.
done
=
state
.
get
(
'done'
,
False
)
self
.
input_state
=
state
.
get
(
'input_state'
,
{})
# Convert startouttext and endouttext to proper <text></text>
problem_text
=
re
.
sub
(
"startouttext
\
s*/"
,
"text"
,
problem_text
)
...
...
@@ -188,6 +189,7 @@ class LoncapaProblem(object):
return
{
'seed'
:
self
.
seed
,
'student_answers'
:
self
.
student_answers
,
'correct_map'
:
self
.
correct_map
.
get_dict
(),
'input_state'
:
self
.
input_state
,
'done'
:
self
.
done
}
def
get_max_score
(
self
):
...
...
@@ -237,6 +239,20 @@ class LoncapaProblem(object):
self
.
correct_map
.
set_dict
(
cmap
.
get_dict
())
return
cmap
def
ungraded_response
(
self
,
xqueue_msg
,
queuekey
):
'''
Handle any responses from the xqueue that do not contain grades
Will try to pass the queue message to all inputtypes that can handle ungraded responses
Does not return any value
'''
# check against each inputtype
for
the_input
in
self
.
inputs
.
values
():
# if the input type has an ungraded function, pass in the values
if
hasattr
(
the_input
,
'ungraded_response'
):
the_input
.
ungraded_response
(
xqueue_msg
,
queuekey
)
def
is_queued
(
self
):
'''
Returns True if any part of the problem has been submitted to an external queue
...
...
@@ -351,7 +367,7 @@ class LoncapaProblem(object):
dispatch
=
get
[
'dispatch'
]
return
self
.
inputs
[
input_id
]
.
handle_ajax
(
dispatch
,
get
)
else
:
log
.
warning
(
"Could not find matching input for id:
%
s"
%
problem
_id
)
log
.
warning
(
"Could not find matching input for id:
%
s"
%
input
_id
)
return
{}
...
...
@@ -527,11 +543,15 @@ class LoncapaProblem(object):
value
=
""
if
self
.
student_answers
and
problemid
in
self
.
student_answers
:
value
=
self
.
student_answers
[
problemid
]
if
input_id
not
in
self
.
input_state
:
self
.
input_state
[
input_id
]
=
{}
# do the rendering
state
=
{
'value'
:
value
,
'status'
:
status
,
'id'
:
input_id
,
'input_state'
:
self
.
input_state
[
input_id
],
'feedback'
:
{
'message'
:
msg
,
'hint'
:
hint
,
'hintmode'
:
hintmode
,
}}
...
...
common/lib/capa/capa/inputtypes.py
View file @
f0e1b477
...
...
@@ -37,18 +37,18 @@ graded status as'status'
# makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a
# general css and layout strategy for capa, document it, then implement it.
from
collections
import
namedtuple
import
json
import
logging
from
lxml
import
etree
import
re
import
shlex
# for splitting quoted strings
import
sys
import
os
import
pyparsing
from
.registry
import
TagRegistry
from
capa.chem
import
chemcalc
import
xqueue_interface
from
datetime
import
datetime
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -97,7 +97,8 @@ class Attribute(object):
"""
val
=
element
.
get
(
self
.
name
)
if
self
.
default
==
self
.
_sentinel
and
val
is
None
:
raise
ValueError
(
'Missing required attribute {0}.'
.
format
(
self
.
name
))
raise
ValueError
(
'Missing required attribute {0}.'
.
format
(
self
.
name
))
if
val
is
None
:
# not required, so return default
...
...
@@ -132,6 +133,8 @@ class InputTypeBase(object):
* 'id' -- the id of this input, typically
"{problem-location}_{response-num}_{input-num}"
* 'status' (answered, unanswered, unsubmitted)
* 'input_state' -- dictionary containing any inputtype-specific state
that has been preserved
* 'feedback' (dictionary containing keys for hints, errors, or other
feedback from previous attempt. Specifically 'message', 'hint',
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
...
...
@@ -149,7 +152,8 @@ class InputTypeBase(object):
self
.
id
=
state
.
get
(
'id'
,
xml
.
get
(
'id'
))
if
self
.
id
is
None
:
raise
ValueError
(
"input id state is None. xml is {0}"
.
format
(
etree
.
tostring
(
xml
)))
raise
ValueError
(
"input id state is None. xml is {0}"
.
format
(
etree
.
tostring
(
xml
)))
self
.
value
=
state
.
get
(
'value'
,
''
)
...
...
@@ -157,6 +161,7 @@ class InputTypeBase(object):
self
.
msg
=
feedback
.
get
(
'message'
,
''
)
self
.
hint
=
feedback
.
get
(
'hint'
,
''
)
self
.
hintmode
=
feedback
.
get
(
'hintmode'
,
None
)
self
.
input_state
=
state
.
get
(
'input_state'
,
{})
# put hint above msg if it should be displayed
if
self
.
hintmode
==
'always'
:
...
...
@@ -169,14 +174,15 @@ class InputTypeBase(object):
self
.
process_requirements
()
# Call subclass "constructor" -- means they don't have to worry about calling
# super().__init__, and are isolated from changes to the input constructor interface.
# super().__init__, and are isolated from changes to the input
# constructor interface.
self
.
setup
()
except
Exception
as
err
:
# Something went wrong: add xml to message, but keep the traceback
msg
=
"Error in xml '{x}': {err} "
.
format
(
x
=
etree
.
tostring
(
xml
),
err
=
str
(
err
))
msg
=
"Error in xml '{x}': {err} "
.
format
(
x
=
etree
.
tostring
(
xml
),
err
=
str
(
err
))
raise
Exception
,
msg
,
sys
.
exc_info
()[
2
]
@classmethod
def
get_attributes
(
cls
):
"""
...
...
@@ -186,7 +192,6 @@ class InputTypeBase(object):
"""
return
[]
def
process_requirements
(
self
):
"""
Subclasses can declare lists of required and optional attributes. This
...
...
@@ -196,7 +201,8 @@ class InputTypeBase(object):
Processes attributes, putting the results in the self.loaded_attributes dictionary. Also creates a set
self.to_render, containing the names of attributes that should be included in the context by default.
"""
# Use local dicts and sets so that if there are exceptions, we don't end up in a partially-initialized state.
# Use local dicts and sets so that if there are exceptions, we don't
# end up in a partially-initialized state.
loaded
=
{}
to_render
=
set
()
for
a
in
self
.
get_attributes
():
...
...
@@ -226,7 +232,7 @@ class InputTypeBase(object):
get: a dictionary containing the data that was sent with the ajax call
Output:
a dictionary object that can be serialized into JSON. This will be sent back to the Javascript.
a dictionary object that can be serialized into JSON. This will be sent back to the Javascript.
"""
pass
...
...
@@ -247,8 +253,9 @@ class InputTypeBase(object):
'value'
:
self
.
value
,
'status'
:
self
.
status
,
'msg'
:
self
.
msg
,
}
context
.
update
((
a
,
v
)
for
(
a
,
v
)
in
self
.
loaded_attributes
.
iteritems
()
if
a
in
self
.
to_render
)
}
context
.
update
((
a
,
v
)
for
(
a
,
v
)
in
self
.
loaded_attributes
.
iteritems
()
if
a
in
self
.
to_render
)
context
.
update
(
self
.
_extra_context
())
return
context
...
...
@@ -371,7 +378,6 @@ class ChoiceGroup(InputTypeBase):
return
[
Attribute
(
"show_correctness"
,
"always"
),
Attribute
(
"submitted_message"
,
"Answer received."
)]
def
_extra_context
(
self
):
return
{
'input_type'
:
self
.
html_input_type
,
'choices'
:
self
.
choices
,
...
...
@@ -436,7 +442,6 @@ class JavascriptInput(InputTypeBase):
Attribute
(
'display_class'
,
None
),
Attribute
(
'display_file'
,
None
),
]
def
setup
(
self
):
# Need to provide a value that JSON can parse if there is no
# student-supplied value yet.
...
...
@@ -459,7 +464,6 @@ class TextLine(InputTypeBase):
template
=
"textline.html"
tags
=
[
'textline'
]
@classmethod
def
get_attributes
(
cls
):
"""
...
...
@@ -474,12 +478,12 @@ class TextLine(InputTypeBase):
# Attributes below used in setup(), not rendered directly.
Attribute
(
'math'
,
None
,
render
=
False
),
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
# TODO: 'dojs' flag is temporary, for backwards compatibility with
# 8.02x
Attribute
(
'dojs'
,
None
,
render
=
False
),
Attribute
(
'preprocessorClassName'
,
None
,
render
=
False
),
Attribute
(
'preprocessorSrc'
,
None
,
render
=
False
),
]
]
def
setup
(
self
):
self
.
do_math
=
bool
(
self
.
loaded_attributes
[
'math'
]
or
...
...
@@ -490,12 +494,12 @@ class TextLine(InputTypeBase):
self
.
preprocessor
=
None
if
self
.
do_math
:
# Preprocessor to insert between raw input and Mathjax
self
.
preprocessor
=
{
'class_name'
:
self
.
loaded_attributes
[
'preprocessorClassName'
],
'script_src'
:
self
.
loaded_attributes
[
'preprocessorSrc'
]}
self
.
preprocessor
=
{
'class_name'
:
self
.
loaded_attributes
[
'preprocessorClassName'
],
'script_src'
:
self
.
loaded_attributes
[
'preprocessorSrc'
]}
if
None
in
self
.
preprocessor
.
values
():
self
.
preprocessor
=
None
def
_extra_context
(
self
):
return
{
'do_math'
:
self
.
do_math
,
'preprocessor'
:
self
.
preprocessor
,
}
...
...
@@ -539,7 +543,8 @@ class FileSubmission(InputTypeBase):
"""
# Check if problem has been queued
self
.
queue_len
=
0
# Flag indicating that the problem has been queued, 'msg' is length of queue
# Flag indicating that the problem has been queued, 'msg' is length of
# queue
if
self
.
status
==
'incomplete'
:
self
.
status
=
'queued'
self
.
queue_len
=
self
.
msg
...
...
@@ -547,7 +552,6 @@ class FileSubmission(InputTypeBase):
def
_extra_context
(
self
):
return
{
'queue_len'
:
self
.
queue_len
,
}
return
context
registry
.
register
(
FileSubmission
)
...
...
@@ -562,8 +566,9 @@ class CodeInput(InputTypeBase):
template
=
"codeinput.html"
tags
=
[
'codeinput'
,
'textbox'
,
# Another (older) name--at some point we may want to make it use a
# non-codemirror editor.
'textbox'
,
# Another (older) name--at some point we may want to make it use a
# non-codemirror editor.
]
# pulled out for testing
...
...
@@ -586,22 +591,29 @@ class CodeInput(InputTypeBase):
Attribute
(
'tabsize'
,
4
,
transform
=
int
),
]
def
setup
(
self
):
def
setup
_code_response_rendering
(
self
):
"""
Implement special logic: handle queueing state, and default input.
"""
# if no student input yet, then use the default input given by the problem
if
not
self
.
value
:
self
.
value
=
self
.
xml
.
text
# if no student input yet, then use the default input given by the
# problem
if
not
self
.
value
and
self
.
xml
.
text
:
self
.
value
=
self
.
xml
.
text
.
strip
()
# Check if problem has been queued
self
.
queue_len
=
0
# Flag indicating that the problem has been queued, 'msg' is length of queue
# Flag indicating that the problem has been queued, 'msg' is length of
# queue
if
self
.
status
==
'incomplete'
:
self
.
status
=
'queued'
self
.
queue_len
=
self
.
msg
self
.
msg
=
self
.
submitted_msg
def
setup
(
self
):
''' setup this input type '''
self
.
setup_code_response_rendering
()
def
_extra_context
(
self
):
"""Defined queue_len, add it """
return
{
'queue_len'
:
self
.
queue_len
,
}
...
...
@@ -610,8 +622,164 @@ registry.register(CodeInput)
#-----------------------------------------------------------------------------
class
MatlabInput
(
CodeInput
):
'''
InputType for handling Matlab code input
TODO: API_KEY will go away once we have a way to specify it per-course
Example:
<matlabinput rows="10" cols="80" tabsize="4">
Initial Text
<plot_payload>
%
api_key=API_KEY
</plot_payload>
</matlabinput>
'''
template
=
"matlabinput.html"
tags
=
[
'matlabinput'
]
plot_submitted_msg
=
(
"Submitted. As soon as a response is returned, "
"this message will be replaced by that feedback."
)
def
setup
(
self
):
'''
Handle matlab-specific parsing
'''
self
.
setup_code_response_rendering
()
xml
=
self
.
xml
self
.
plot_payload
=
xml
.
findtext
(
'./plot_payload'
)
# Check if problem has been queued
self
.
queuename
=
'matlab'
self
.
queue_msg
=
''
if
'queue_msg'
in
self
.
input_state
and
self
.
status
in
[
'queued'
,
'incomplete'
,
'unsubmitted'
]:
self
.
queue_msg
=
self
.
input_state
[
'queue_msg'
]
if
'queued'
in
self
.
input_state
and
self
.
input_state
[
'queuestate'
]
is
not
None
:
self
.
status
=
'queued'
self
.
queue_len
=
1
self
.
msg
=
self
.
plot_submitted_msg
def
handle_ajax
(
self
,
dispatch
,
get
):
'''
Handle AJAX calls directed to this input
Args:
- dispatch (str) - indicates how we want this ajax call to be handled
- get (dict) - dictionary of key-value pairs that contain useful data
Returns:
'''
if
dispatch
==
'plot'
:
return
self
.
_plot_data
(
get
)
return
{}
def
ungraded_response
(
self
,
queue_msg
,
queuekey
):
'''
Handle the response from the XQueue
Stores the response in the input_state so it can be rendered later
Args:
- queue_msg (str) - message returned from the queue. The message to be rendered
- queuekey (str) - a key passed to the queue. Will be matched up to verify that this is the response we're waiting for
Returns:
nothing
'''
# check the queuekey against the saved queuekey
if
(
'queuestate'
in
self
.
input_state
and
self
.
input_state
[
'queuestate'
]
==
'queued'
and
self
.
input_state
[
'queuekey'
]
==
queuekey
):
msg
=
self
.
_parse_data
(
queue_msg
)
# save the queue message so that it can be rendered later
self
.
input_state
[
'queue_msg'
]
=
msg
self
.
input_state
[
'queuestate'
]
=
None
self
.
input_state
[
'queuekey'
]
=
None
def
_extra_context
(
self
):
''' Set up additional context variables'''
extra_context
=
{
'queue_len'
:
self
.
queue_len
,
'queue_msg'
:
self
.
queue_msg
}
return
extra_context
def
_parse_data
(
self
,
queue_msg
):
'''
Parses the message out of the queue message
Args:
queue_msg (str) - a JSON encoded string
Returns:
returns the value for the the key 'msg' in queue_msg
'''
try
:
result
=
json
.
loads
(
queue_msg
)
except
(
TypeError
,
ValueError
):
log
.
error
(
"External message should be a JSON serialized dict."
" Received queue_msg =
%
s"
%
queue_msg
)
raise
msg
=
result
[
'msg'
]
return
msg
def
_plot_data
(
self
,
get
):
'''
AJAX handler for the plot button
Args:
get (dict) - should have key 'submission' which contains the student submission
Returns:
dict - 'success' - whether or not we successfully queued this submission
- 'message' - message to be rendered in case of error
'''
# only send data if xqueue exists
if
self
.
system
.
xqueue
is
None
:
return
{
'success'
:
False
,
'message'
:
'Cannot connect to the queue'
}
# pull relevant info out of get
response
=
get
[
'submission'
]
# construct xqueue headers
qinterface
=
self
.
system
.
xqueue
[
'interface'
]
qtime
=
datetime
.
strftime
(
datetime
.
utcnow
(),
xqueue_interface
.
dateformat
)
callback_url
=
self
.
system
.
xqueue
[
'construct_callback'
](
'ungraded_response'
)
anonymous_student_id
=
self
.
system
.
anonymous_student_id
queuekey
=
xqueue_interface
.
make_hashkey
(
str
(
self
.
system
.
seed
)
+
qtime
+
anonymous_student_id
+
self
.
id
)
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
callback_url
,
lms_key
=
queuekey
,
queue_name
=
self
.
queuename
)
# save the input state
self
.
input_state
[
'queuekey'
]
=
queuekey
self
.
input_state
[
'queuestate'
]
=
'queued'
# construct xqueue body
student_info
=
{
'anonymous_student_id'
:
anonymous_student_id
,
'submission_time'
:
qtime
}
contents
=
{
'grader_payload'
:
self
.
plot_payload
,
'student_info'
:
json
.
dumps
(
student_info
),
'student_response'
:
response
}
(
error
,
msg
)
=
qinterface
.
send_to_queue
(
header
=
xheader
,
body
=
json
.
dumps
(
contents
))
return
{
'success'
:
error
==
0
,
'message'
:
msg
}
registry
.
register
(
MatlabInput
)
#-----------------------------------------------------------------------------
class
Schematic
(
InputTypeBase
):
"""
InputType for the schematic editor
"""
template
=
"schematicinput.html"
...
...
@@ -630,7 +798,6 @@ class Schematic(InputTypeBase):
Attribute
(
'initial_value'
,
None
),
Attribute
(
'submit_analyses'
,
None
),
]
return
context
registry
.
register
(
Schematic
)
...
...
@@ -660,12 +827,12 @@ class ImageInput(InputTypeBase):
Attribute
(
'height'
),
Attribute
(
'width'
),
]
def
setup
(
self
):
"""
if value is of the form [x,y] then parse it and send along coordinates of previous answer
"""
m
=
re
.
match
(
'
\
[([0-9]+),([0-9]+)]'
,
self
.
value
.
strip
()
.
replace
(
' '
,
''
))
m
=
re
.
match
(
'
\
[([0-9]+),([0-9]+)]'
,
self
.
value
.
strip
()
.
replace
(
' '
,
''
))
if
m
:
# Note: we subtract 15 to compensate for the size of the dot on the screen.
# (is a 30x30 image--lms/static/green-pointer.png).
...
...
@@ -673,7 +840,6 @@ class ImageInput(InputTypeBase):
else
:
(
self
.
gx
,
self
.
gy
)
=
(
0
,
0
)
def
_extra_context
(
self
):
return
{
'gx'
:
self
.
gx
,
...
...
@@ -730,7 +896,7 @@ class VseprInput(InputTypeBase):
registry
.
register
(
VseprInput
)
#-------------------------------------------------------------------------
-------
#-------------------------------------------------------------------------
class
ChemicalEquationInput
(
InputTypeBase
):
...
...
@@ -794,7 +960,8 @@ class ChemicalEquationInput(InputTypeBase):
result
[
'error'
]
=
"Couldn't parse formula: {0}"
.
format
(
p
)
except
Exception
:
# this is unexpected, so log
log
.
warning
(
"Error while previewing chemical formula"
,
exc_info
=
True
)
log
.
warning
(
"Error while previewing chemical formula"
,
exc_info
=
True
)
result
[
'error'
]
=
"Error while rendering preview"
return
result
...
...
@@ -843,16 +1010,16 @@ class DragAndDropInput(InputTypeBase):
'can_reuse'
:
""
}
tag_attrs
[
'target'
]
=
{
'id'
:
Attribute
.
_sentinel
,
'x'
:
Attribute
.
_sentinel
,
'y'
:
Attribute
.
_sentinel
,
'w'
:
Attribute
.
_sentinel
,
'h'
:
Attribute
.
_sentinel
}
'x'
:
Attribute
.
_sentinel
,
'y'
:
Attribute
.
_sentinel
,
'w'
:
Attribute
.
_sentinel
,
'h'
:
Attribute
.
_sentinel
}
dic
=
dict
()
for
attr_name
in
tag_attrs
[
tag_type
]
.
keys
():
dic
[
attr_name
]
=
Attribute
(
attr_name
,
default
=
tag_attrs
[
tag_type
][
attr_name
])
.
parse_from_xml
(
tag
)
default
=
tag_attrs
[
tag_type
][
attr_name
])
.
parse_from_xml
(
tag
)
if
tag_type
==
'draggable'
and
not
self
.
no_labels
:
dic
[
'label'
]
=
dic
[
'label'
]
or
dic
[
'id'
]
...
...
@@ -865,7 +1032,7 @@ class DragAndDropInput(InputTypeBase):
# add labels to images?:
self
.
no_labels
=
Attribute
(
'no_labels'
,
default
=
"False"
)
.
parse_from_xml
(
self
.
xml
)
default
=
"False"
)
.
parse_from_xml
(
self
.
xml
)
to_js
=
dict
()
...
...
@@ -874,16 +1041,16 @@ class DragAndDropInput(InputTypeBase):
# outline places on image where to drag adn drop
to_js
[
'target_outline'
]
=
Attribute
(
'target_outline'
,
default
=
"False"
)
.
parse_from_xml
(
self
.
xml
)
default
=
"False"
)
.
parse_from_xml
(
self
.
xml
)
# one draggable per target?
to_js
[
'one_per_target'
]
=
Attribute
(
'one_per_target'
,
default
=
"True"
)
.
parse_from_xml
(
self
.
xml
)
default
=
"True"
)
.
parse_from_xml
(
self
.
xml
)
# list of draggables
to_js
[
'draggables'
]
=
[
parse
(
draggable
,
'draggable'
)
for
draggable
in
self
.
xml
.
iterchildren
(
'draggable'
)]
self
.
xml
.
iterchildren
(
'draggable'
)]
# list of targets
to_js
[
'targets'
]
=
[
parse
(
target
,
'target'
)
for
target
in
self
.
xml
.
iterchildren
(
'target'
)]
self
.
xml
.
iterchildren
(
'target'
)]
# custom background color for labels:
label_bg_color
=
Attribute
(
'label_bg_color'
,
...
...
@@ -896,7 +1063,7 @@ class DragAndDropInput(InputTypeBase):
registry
.
register
(
DragAndDropInput
)
#-------------------------------------------------------------------------
-------------------------------------------
#-------------------------------------------------------------------------
class
EditAMoleculeInput
(
InputTypeBase
):
...
...
@@ -934,6 +1101,7 @@ registry.register(EditAMoleculeInput)
#-----------------------------------------------------------------------------
class
DesignProtein2dInput
(
InputTypeBase
):
"""
An input type for design of a protein in 2D. Integrates with the Protex java applet.
...
...
@@ -969,6 +1137,7 @@ registry.register(DesignProtein2dInput)
#-----------------------------------------------------------------------------
class
EditAGeneInput
(
InputTypeBase
):
"""
An input type for editing a gene. Integrates with the genex java applet.
...
...
@@ -1005,6 +1174,7 @@ registry.register(EditAGeneInput)
#---------------------------------------------------------------------
class
AnnotationInput
(
InputTypeBase
):
"""
Input type for annotations: students can enter some notes or other text
...
...
@@ -1037,13 +1207,14 @@ class AnnotationInput(InputTypeBase):
def
setup
(
self
):
xml
=
self
.
xml
self
.
debug
=
False
# set to True to display extra debug info with input
self
.
return_to_annotation
=
True
# return only works in conjunction with annotatable xmodule
self
.
debug
=
False
# set to True to display extra debug info with input
self
.
return_to_annotation
=
True
# return only works in conjunction with annotatable xmodule
self
.
title
=
xml
.
findtext
(
'./title'
,
'Annotation Exercise'
)
self
.
text
=
xml
.
findtext
(
'./text'
)
self
.
comment
=
xml
.
findtext
(
'./comment'
)
self
.
comment_prompt
=
xml
.
findtext
(
'./comment_prompt'
,
'Type a commentary below:'
)
self
.
comment_prompt
=
xml
.
findtext
(
'./comment_prompt'
,
'Type a commentary below:'
)
self
.
tag_prompt
=
xml
.
findtext
(
'./tag_prompt'
,
'Select one tag:'
)
self
.
options
=
self
.
_find_options
()
...
...
@@ -1061,7 +1232,7 @@ class AnnotationInput(InputTypeBase):
'id'
:
index
,
'description'
:
option
.
text
,
'choice'
:
option
.
get
(
'choice'
)
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
def
_validate_options
(
self
):
''' Raises a ValueError if the choice attribute is missing or invalid. '''
...
...
@@ -1071,7 +1242,8 @@ class AnnotationInput(InputTypeBase):
if
choice
is
None
:
raise
ValueError
(
'Missing required choice attribute.'
)
elif
choice
not
in
valid_choices
:
raise
ValueError
(
'Invalid choice attribute: {0}. Must be one of: {1}'
.
format
(
choice
,
', '
.
join
(
valid_choices
)))
raise
ValueError
(
'Invalid choice attribute: {0}. Must be one of: {1}'
.
format
(
choice
,
', '
.
join
(
valid_choices
)))
def
_unpack
(
self
,
json_value
):
''' Unpacks the json input state into a dict. '''
...
...
@@ -1089,20 +1261,20 @@ class AnnotationInput(InputTypeBase):
return
{
'options_value'
:
options_value
,
'has_options_value'
:
len
(
options_value
)
>
0
,
# for convenience
'has_options_value'
:
len
(
options_value
)
>
0
,
# for convenience
'comment_value'
:
comment_value
,
}
def
_extra_context
(
self
):
extra_context
=
{
'title'
:
self
.
title
,
'text'
:
self
.
text
,
'comment'
:
self
.
comment
,
'comment_prompt'
:
self
.
comment_prompt
,
'tag_prompt'
:
self
.
tag_prompt
,
'options'
:
self
.
options
,
'return_to_annotation'
:
self
.
return_to_annotation
,
'debug'
:
self
.
debug
'title'
:
self
.
title
,
'text'
:
self
.
text
,
'comment'
:
self
.
comment
,
'comment_prompt'
:
self
.
comment_prompt
,
'tag_prompt'
:
self
.
tag_prompt
,
'options'
:
self
.
options
,
'return_to_annotation'
:
self
.
return_to_annotation
,
'debug'
:
self
.
debug
}
extra_context
.
update
(
self
.
_unpack
(
self
.
value
))
...
...
@@ -1110,4 +1282,3 @@ class AnnotationInput(InputTypeBase):
return
extra_context
registry
.
register
(
AnnotationInput
)
common/lib/capa/capa/responsetypes.py
View file @
f0e1b477
...
...
@@ -128,21 +128,25 @@ class LoncapaResponse(object):
for
abox
in
inputfields
:
if
abox
.
tag
not
in
self
.
allowed_inputfields
:
msg
=
"
%
s: cannot have input field
%
s"
%
(
unicode
(
self
),
abox
.
tag
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
msg
=
"
%
s: cannot have input field
%
s"
%
(
unicode
(
self
),
abox
.
tag
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
if
self
.
max_inputfields
and
len
(
inputfields
)
>
self
.
max_inputfields
:
msg
=
"
%
s: cannot have more than
%
s input fields"
%
(
unicode
(
self
),
self
.
max_inputfields
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
for
prop
in
self
.
required_attributes
:
if
not
xml
.
get
(
prop
):
msg
=
"Error in problem specification:
%
s missing required attribute
%
s"
%
(
unicode
(
self
),
prop
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
# ordered list of answer_id values for this response
...
...
@@ -163,7 +167,8 @@ class LoncapaResponse(object):
for
entry
in
self
.
inputfields
:
answer
=
entry
.
get
(
'correct_answer'
)
if
answer
:
self
.
default_answer_map
[
entry
.
get
(
'id'
)]
=
contextualize_text
(
answer
,
self
.
context
)
self
.
default_answer_map
[
entry
.
get
(
'id'
)]
=
contextualize_text
(
answer
,
self
.
context
)
if
hasattr
(
self
,
'setup_response'
):
self
.
setup_response
()
...
...
@@ -211,7 +216,8 @@ class LoncapaResponse(object):
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
'''
new_cmap
=
self
.
get_score
(
student_answers
)
self
.
get_hints
(
convert_files_to_filenames
(
student_answers
),
new_cmap
,
old_cmap
)
self
.
get_hints
(
convert_files_to_filenames
(
student_answers
),
new_cmap
,
old_cmap
)
# log.debug('new_cmap = %s' % new_cmap)
return
new_cmap
...
...
@@ -241,14 +247,17 @@ class LoncapaResponse(object):
# callback procedure to a social hint generation system.
if
not
hintfn
in
self
.
context
:
msg
=
'missing specified hint function
%
s in script context'
%
hintfn
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
try
:
self
.
context
[
hintfn
](
self
.
answer_ids
,
student_answers
,
new_cmap
,
old_cmap
)
self
.
context
[
hintfn
](
self
.
answer_ids
,
student_answers
,
new_cmap
,
old_cmap
)
except
Exception
as
err
:
msg
=
'Error
%
s in evaluating hint function
%
s'
%
(
err
,
hintfn
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
ResponseError
(
msg
)
return
...
...
@@ -270,17 +279,19 @@ class LoncapaResponse(object):
if
(
self
.
hint_tag
is
not
None
and
hintgroup
.
find
(
self
.
hint_tag
)
is
not
None
and
hasattr
(
self
,
'check_hint_condition'
)):
and
hasattr
(
self
,
'check_hint_condition'
)):
rephints
=
hintgroup
.
findall
(
self
.
hint_tag
)
hints_to_show
=
self
.
check_hint_condition
(
rephints
,
student_answers
)
hints_to_show
=
self
.
check_hint_condition
(
rephints
,
student_answers
)
# can be 'on_request' or 'always' (default)
hintmode
=
hintgroup
.
get
(
'mode'
,
'always'
)
for
hintpart
in
hintgroup
.
findall
(
'hintpart'
):
if
hintpart
.
get
(
'on'
)
in
hints_to_show
:
hint_text
=
hintpart
.
find
(
'text'
)
.
text
# make the hint appear after the last answer box in this response
# make the hint appear after the last answer box in this
# response
aid
=
self
.
answer_ids
[
-
1
]
new_cmap
.
set_hint_and_mode
(
aid
,
hint_text
,
hintmode
)
log
.
debug
(
'after hint: new_cmap =
%
s'
%
new_cmap
)
...
...
@@ -340,7 +351,6 @@ class LoncapaResponse(object):
response_msg_div
=
etree
.
Element
(
'div'
)
response_msg_div
.
text
=
str
(
response_msg
)
# Set the css class of the message <div>
response_msg_div
.
set
(
"class"
,
"response_message"
)
...
...
@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse):
# until we decide on exactly how to solve this issue. For now, files are
# manually being compiled to DATA_DIR/js/compiled.
#latestTimestamp = 0
#basepath = self.system.filestore.root_path + '/js/'
#for filename in (self.display_dependencies + [self.display]):
#
latestTimestamp = 0
#
basepath = self.system.filestore.root_path + '/js/'
#
for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
# timestamp = os.stat(filepath).st_mtime
# if timestamp > latestTimestamp:
# latestTimestamp = timestamp
#
#h = hashlib.md5()
#h.update(self.answer_id + str(self.display_dependencies))
#compiled_filename = 'compiled/' + h.hexdigest() + '.js'
#compiled_filepath = basepath + compiled_filename
#
h = hashlib.md5()
#
h.update(self.answer_id + str(self.display_dependencies))
#
compiled_filename = 'compiled/' + h.hexdigest() + '.js'
#
compiled_filepath = basepath + compiled_filename
#if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
#
if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
# outfile = open(compiled_filepath, 'w')
# for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
...
...
@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse):
id
=
self
.
xml
.
get
(
'id'
))[
0
]
self
.
display_xml
=
self
.
xml
.
xpath
(
'//*[@id=$id]//display'
,
id
=
self
.
xml
.
get
(
'id'
))[
0
]
id
=
self
.
xml
.
get
(
'id'
))[
0
]
self
.
xml
.
remove
(
self
.
generator_xml
)
self
.
xml
.
remove
(
self
.
grader_xml
)
...
...
@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse):
self
.
display
=
self
.
display_xml
.
get
(
"src"
)
if
self
.
generator_xml
.
get
(
"dependencies"
):
self
.
generator_dependencies
=
self
.
generator_xml
.
get
(
"dependencies"
)
.
split
()
self
.
generator_dependencies
=
self
.
generator_xml
.
get
(
"dependencies"
)
.
split
()
else
:
self
.
generator_dependencies
=
[]
if
self
.
grader_xml
.
get
(
"dependencies"
):
self
.
grader_dependencies
=
self
.
grader_xml
.
get
(
"dependencies"
)
.
split
()
self
.
grader_dependencies
=
self
.
grader_xml
.
get
(
"dependencies"
)
.
split
()
else
:
self
.
grader_dependencies
=
[]
if
self
.
display_xml
.
get
(
"dependencies"
):
self
.
display_dependencies
=
self
.
display_xml
.
get
(
"dependencies"
)
.
split
()
self
.
display_dependencies
=
self
.
display_xml
.
get
(
"dependencies"
)
.
split
()
else
:
self
.
display_dependencies
=
[]
...
...
@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse):
return
subprocess
.
check_output
(
subprocess_args
,
env
=
self
.
get_node_env
())
def
generate_problem_state
(
self
):
generator_file
=
os
.
path
.
dirname
(
os
.
path
.
normpath
(
__file__
))
+
'/javascript_problem_generator.js'
generator_file
=
os
.
path
.
dirname
(
os
.
path
.
normpath
(
__file__
))
+
'/javascript_problem_generator.js'
output
=
self
.
call_node
([
generator_file
,
self
.
generator
,
json
.
dumps
(
self
.
generator_dependencies
),
...
...
@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse):
params
=
{}
for
param
in
self
.
xml
.
xpath
(
'//*[@id=$id]//responseparam'
,
id
=
self
.
xml
.
get
(
'id'
)):
id
=
self
.
xml
.
get
(
'id'
)):
raw_param
=
param
.
get
(
"value"
)
params
[
param
.
get
(
"name"
)]
=
json
.
loads
(
contextualize_text
(
raw_param
,
self
.
context
))
params
[
param
.
get
(
"name"
)]
=
json
.
loads
(
contextualize_text
(
raw_param
,
self
.
context
))
return
params
def
prepare_inputfield
(
self
):
for
inputfield
in
self
.
xml
.
xpath
(
'//*[@id=$id]//javascriptinput'
,
id
=
self
.
xml
.
get
(
'id'
)):
id
=
self
.
xml
.
get
(
'id'
)):
escapedict
=
{
'"'
:
'"'
}
...
...
@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse):
escapedict
)
inputfield
.
set
(
"problem_state"
,
encoded_problem_state
)
inputfield
.
set
(
"display_file"
,
self
.
display_filename
)
inputfield
.
set
(
"display_file"
,
self
.
display_filename
)
inputfield
.
set
(
"display_class"
,
self
.
display_class
)
def
get_score
(
self
,
student_answers
):
...
...
@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse):
if
submission
is
None
or
submission
==
''
:
submission
=
json
.
dumps
(
None
)
grader_file
=
os
.
path
.
dirname
(
os
.
path
.
normpath
(
__file__
))
+
'/javascript_problem_grader.js'
grader_file
=
os
.
path
.
dirname
(
os
.
path
.
normpath
(
__file__
))
+
'/javascript_problem_grader.js'
outputs
=
self
.
call_node
([
grader_file
,
self
.
grader
,
json
.
dumps
(
self
.
grader_dependencies
),
...
...
@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse):
json
.
dumps
(
self
.
params
)])
.
split
(
'
\n
'
)
all_correct
=
json
.
loads
(
outputs
[
0
]
.
strip
())
evaluation
=
outputs
[
1
]
.
strip
()
solution
=
outputs
[
2
]
.
strip
()
evaluation
=
outputs
[
1
]
.
strip
()
solution
=
outputs
[
2
]
.
strip
()
return
(
all_correct
,
evaluation
,
solution
)
def
get_answers
(
self
):
...
...
@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse):
return
{
self
.
answer_id
:
self
.
solution
}
#-----------------------------------------------------------------------------
class
ChoiceResponse
(
LoncapaResponse
):
"""
This response type is used when the student chooses from a discrete set of
...
...
@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse):
self
.
assign_choice_names
()
correct_xml
=
self
.
xml
.
xpath
(
'//*[@id=$id]//choice[@correct="true"]'
,
id
=
self
.
xml
.
get
(
'id'
))
id
=
self
.
xml
.
get
(
'id'
))
self
.
correct_choices
=
set
([
choice
.
get
(
'name'
)
for
choice
in
correct_xml
])
self
.
correct_choices
=
set
([
choice
.
get
(
'name'
)
for
choice
in
correct_xml
])
def
assign_choice_names
(
self
):
'''
...
...
@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse):
allowed_inputfields
=
[
'choicegroup'
]
def
setup_response
(
self
):
# call secondary setup for MultipleChoice questions, to set name attributes
# call secondary setup for MultipleChoice questions, to set name
# attributes
self
.
mc_setup_response
()
# define correct choices (after calling secondary setup)
...
...
@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
# unicode(self), student_answers, self.correct_choices))
if
(
self
.
answer_id
in
student_answers
and
student_answers
[
self
.
answer_id
]
in
self
.
correct_choices
):
and
student_answers
[
self
.
answer_id
]
in
self
.
correct_choices
):
return
CorrectMap
(
self
.
answer_id
,
'correct'
)
else
:
return
CorrectMap
(
self
.
answer_id
,
'incorrect'
)
...
...
@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse):
return
cmap
def
get_answers
(
self
):
amap
=
dict
([(
af
.
get
(
'id'
),
contextualize_text
(
af
.
get
(
'correct'
),
self
.
context
))
for
af
in
self
.
answer_fields
])
amap
=
dict
([(
af
.
get
(
'id'
),
contextualize_text
(
af
.
get
(
'correct'
),
self
.
context
))
for
af
in
self
.
answer_fields
])
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
return
amap
...
...
@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse):
context
=
self
.
context
self
.
correct_answer
=
contextualize_text
(
xml
.
get
(
'answer'
),
context
)
try
:
self
.
tolerance_xml
=
xml
.
xpath
(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
id
=
xml
.
get
(
'id'
))[
0
]
self
.
tolerance_xml
=
xml
.
xpath
(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
id
=
xml
.
get
(
'id'
))[
0
]
self
.
tolerance
=
contextualize_text
(
self
.
tolerance_xml
,
context
)
except
Exception
:
self
.
tolerance
=
'0'
...
...
@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse):
try
:
correct_ans
=
complex
(
self
.
correct_answer
)
except
ValueError
:
log
.
debug
(
"Content error--answer '{0}' is not a valid complex number"
.
format
(
self
.
correct_answer
))
raise
StudentInputError
(
"There was a problem with the staff answer to this problem"
)
log
.
debug
(
"Content error--answer '{0}' is not a valid complex number"
.
format
(
self
.
correct_answer
))
raise
StudentInputError
(
"There was a problem with the staff answer to this problem"
)
try
:
correct
=
compare_with_tolerance
(
evaluator
(
dict
(),
dict
(),
student_answer
),
correct_ans
,
self
.
tolerance
)
correct
=
compare_with_tolerance
(
evaluator
(
dict
(),
dict
(),
student_answer
),
correct_ans
,
self
.
tolerance
)
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
except
:
# Use the traceback-preserving version of re-raising with a different type
# Use the traceback-preserving version of re-raising with a
# different type
import
sys
type
,
value
,
traceback
=
sys
.
exc_info
()
...
...
@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse):
max_inputfields
=
1
def
setup_response
(
self
):
self
.
correct_answer
=
contextualize_text
(
self
.
xml
.
get
(
'answer'
),
self
.
context
)
.
strip
()
self
.
correct_answer
=
contextualize_text
(
self
.
xml
.
get
(
'answer'
),
self
.
context
)
.
strip
()
def
get_score
(
self
,
student_answers
):
'''Grade a string response '''
...
...
@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse):
return
CorrectMap
(
self
.
answer_id
,
'correct'
if
correct
else
'incorrect'
)
def
check_string
(
self
,
expected
,
given
):
if
self
.
xml
.
get
(
'type'
)
==
'ci'
:
return
given
.
lower
()
==
expected
.
lower
()
if
self
.
xml
.
get
(
'type'
)
==
'ci'
:
return
given
.
lower
()
==
expected
.
lower
()
return
given
==
expected
def
check_hint_condition
(
self
,
hxml_set
,
student_answers
):
...
...
@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse):
hints_to_show
=
[]
for
hxml
in
hxml_set
:
name
=
hxml
.
get
(
'name'
)
correct_answer
=
contextualize_text
(
hxml
.
get
(
'answer'
),
self
.
context
)
.
strip
()
if
self
.
check_string
(
correct_answer
,
given
):
hints_to_show
.
append
(
name
)
correct_answer
=
contextualize_text
(
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
)
return
hints_to_show
...
...
@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse):
correct[0] ='incorrect'
</answer>
</customresponse>"""
},
{
'snippet'
:
"""<script type="loncapa/python"><![CDATA[
{
'snippet'
:
"""<script type="loncapa/python"><![CDATA[
def sympy_check2():
messages[0] = '
%
s:
%
s'
%
(submission[0],fromjs[0].replace('<','<'))
...
...
@@ -907,15 +932,16 @@ def sympy_check2():
response_tag
=
'customresponse'
allowed_inputfields
=
[
'textline'
,
'textbox'
,
'crystallography'
,
'chemicalequationinput'
,
'vsepr_input'
,
'drag_and_drop_input'
,
'editamoleculeinput'
,
'designprotein2dinput'
,
'editageneinput'
,
'annotationinput'
]
'chemicalequationinput'
,
'vsepr_input'
,
'drag_and_drop_input'
,
'editamoleculeinput'
,
'designprotein2dinput'
,
'editageneinput'
,
'annotationinput'
]
def
setup_response
(
self
):
xml
=
self
.
xml
# if <customresponse> has an "expect" (or "answer") attribute then save that
# if <customresponse> has an "expect" (or "answer") attribute then save
# that
self
.
expect
=
xml
.
get
(
'expect'
)
or
xml
.
get
(
'answer'
)
self
.
myid
=
xml
.
get
(
'id'
)
...
...
@@ -939,7 +965,8 @@ def sympy_check2():
if
cfn
in
self
.
context
:
self
.
code
=
self
.
context
[
cfn
]
else
:
msg
=
"
%
s: can't find cfn
%
s in context"
%
(
unicode
(
self
),
cfn
)
msg
=
"
%
s: can't find cfn
%
s in context"
%
(
unicode
(
self
),
cfn
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
...
...
@@ -952,7 +979,8 @@ def sympy_check2():
else
:
answer_src
=
answer
.
get
(
'src'
)
if
answer_src
is
not
None
:
self
.
code
=
self
.
system
.
filesystem
.
open
(
'src/'
+
answer_src
)
.
read
()
self
.
code
=
self
.
system
.
filesystem
.
open
(
'src/'
+
answer_src
)
.
read
()
else
:
self
.
code
=
answer
.
text
...
...
@@ -1032,7 +1060,7 @@ def sympy_check2():
# any options to be passed to the cfn
'options'
:
self
.
xml
.
get
(
'options'
),
'testdat'
:
'hello world'
,
})
})
# pass self.system.debug to cfn
self
.
context
[
'debug'
]
=
self
.
system
.
DEBUG
...
...
@@ -1049,7 +1077,8 @@ def sympy_check2():
print
"context = "
,
self
.
context
print
traceback
.
format_exc
()
# Notify student
raise
StudentInputError
(
"Error: Problem could not be evaluated with your input"
)
raise
StudentInputError
(
"Error: Problem could not be evaluated with your input"
)
else
:
# self.code is not a string; assume its a function
...
...
@@ -1058,18 +1087,22 @@ def sympy_check2():
ret
=
None
log
.
debug
(
" submission =
%
s"
%
submission
)
try
:
answer_given
=
submission
[
0
]
if
(
len
(
idset
)
==
1
)
else
submission
answer_given
=
submission
[
0
]
if
(
len
(
idset
)
==
1
)
else
submission
# handle variable number of arguments in check function, for backwards compatibility
# with various Tutor2 check functions
args
=
[
self
.
expect
,
answer_given
,
student_answers
,
self
.
answer_ids
[
0
]]
args
=
[
self
.
expect
,
answer_given
,
student_answers
,
self
.
answer_ids
[
0
]]
argspec
=
inspect
.
getargspec
(
fn
)
nargs
=
len
(
argspec
.
args
)
-
len
(
argspec
.
defaults
or
[])
kwargs
=
{}
for
argname
in
argspec
.
args
[
nargs
:]:
kwargs
[
argname
]
=
self
.
context
[
argname
]
if
argname
in
self
.
context
else
None
kwargs
[
argname
]
=
self
.
context
[
argname
]
if
argname
in
self
.
context
else
None
log
.
debug
(
'[customresponse] answer_given=
%
s'
%
answer_given
)
log
.
debug
(
'nargs=
%
d, args=
%
s, kwargs=
%
s'
%
(
nargs
,
args
,
kwargs
))
log
.
debug
(
'nargs=
%
d, args=
%
s, kwargs=
%
s'
%
(
nargs
,
args
,
kwargs
))
ret
=
fn
(
*
args
[:
nargs
],
**
kwargs
)
except
Exception
as
err
:
...
...
@@ -1077,7 +1110,8 @@ def sympy_check2():
# print "context = ",self.context
log
.
error
(
traceback
.
format_exc
())
raise
Exception
(
"oops in customresponse (cfn) error
%
s"
%
err
)
log
.
debug
(
"[courseware.capa.responsetypes.customresponse.get_score] ret =
%
s"
%
ret
)
log
.
debug
(
"[courseware.capa.responsetypes.customresponse.get_score] ret =
%
s"
%
ret
)
if
type
(
ret
)
==
dict
:
...
...
@@ -1086,7 +1120,8 @@ def sympy_check2():
# If there are multiple inputs, they all get marked
# to the same correct/incorrect value
if
'ok'
in
ret
:
correct
=
[
'correct'
]
*
len
(
idset
)
if
ret
[
'ok'
]
else
[
'incorrect'
]
*
len
(
idset
)
correct
=
[
'correct'
]
*
len
(
idset
)
if
ret
[
'ok'
]
else
[
'incorrect'
]
*
len
(
idset
)
msg
=
ret
.
get
(
'msg'
,
None
)
msg
=
self
.
clean_message_html
(
msg
)
...
...
@@ -1097,7 +1132,6 @@ def sympy_check2():
else
:
messages
[
0
]
=
msg
# Another kind of dictionary the check function can return has
# the form:
# {'overall_message': STRING,
...
...
@@ -1113,21 +1147,25 @@ def sympy_check2():
correct
=
[]
messages
=
[]
for
input_dict
in
input_list
:
correct
.
append
(
'correct'
if
input_dict
[
'ok'
]
else
'incorrect'
)
msg
=
self
.
clean_message_html
(
input_dict
[
'msg'
])
if
'msg'
in
input_dict
else
None
correct
.
append
(
'correct'
if
input_dict
[
'ok'
]
else
'incorrect'
)
msg
=
(
self
.
clean_message_html
(
input_dict
[
'msg'
])
if
'msg'
in
input_dict
else
None
)
messages
.
append
(
msg
)
# Otherwise, we do not recognize the dictionary
# Raise an exception
else
:
log
.
error
(
traceback
.
format_exc
())
raise
Exception
(
"CustomResponse: check function returned an invalid dict"
)
raise
Exception
(
"CustomResponse: check function returned an invalid dict"
)
# The check function can return a boolean value,
# indicating whether all inputs should be marked
# correct or incorrect
else
:
correct
=
[
'correct'
]
*
len
(
idset
)
if
ret
else
[
'incorrect'
]
*
len
(
idset
)
n
=
len
(
idset
)
correct
=
[
'correct'
]
*
n
if
ret
else
[
'incorrect'
]
*
n
# build map giving "correct"ness of the answer(s)
correct_map
=
CorrectMap
()
...
...
@@ -1136,7 +1174,8 @@ def sympy_check2():
correct_map
.
set_overall_message
(
overall_message
)
for
k
in
range
(
len
(
idset
)):
npoints
=
self
.
maxpoints
[
idset
[
k
]]
if
correct
[
k
]
==
'correct'
else
0
npoints
=
(
self
.
maxpoints
[
idset
[
k
]]
if
correct
[
k
]
==
'correct'
else
0
)
correct_map
.
set
(
idset
[
k
],
correct
[
k
],
msg
=
messages
[
k
],
npoints
=
npoints
)
return
correct_map
...
...
@@ -1232,8 +1271,9 @@ class CodeResponse(LoncapaResponse):
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
system.xqueue = { 'interface': XqueueInterface object,
'callback_url': Per-StudentModule callback URL
where results are posted (string),
'construct_callback': Per-StudentModule callback URL
constructor, defaults to using 'score_update'
as the correct dispatch (function),
'default_queuename': Default queuename to submit request (string)
}
...
...
@@ -1242,7 +1282,7 @@ class CodeResponse(LoncapaResponse):
"""
response_tag
=
'coderesponse'
allowed_inputfields
=
[
'textbox'
,
'filesubmission'
]
allowed_inputfields
=
[
'textbox'
,
'filesubmission'
,
'matlabinput'
]
max_inputfields
=
1
def
setup_response
(
self
):
...
...
@@ -1263,7 +1303,8 @@ class CodeResponse(LoncapaResponse):
self
.
queue_name
=
xml
.
get
(
'queuename'
,
default_queuename
)
# VS[compat]:
# Check if XML uses the ExternalResponse format or the generic CodeResponse format
# Check if XML uses the ExternalResponse format or the generic
# CodeResponse format
codeparam
=
self
.
xml
.
find
(
'codeparam'
)
if
codeparam
is
None
:
self
.
_parse_externalresponse_xml
()
...
...
@@ -1277,12 +1318,14 @@ class CodeResponse(LoncapaResponse):
self.answer (an answer to display to the student in the LMS)
self.payload
'''
# Note that CodeResponse is agnostic to the specific contents of grader_payload
# Note that CodeResponse is agnostic to the specific contents of
# grader_payload
grader_payload
=
codeparam
.
find
(
'grader_payload'
)
grader_payload
=
grader_payload
.
text
if
grader_payload
is
not
None
else
''
self
.
payload
=
{
'grader_payload'
:
grader_payload
}
self
.
initial_display
=
find_with_default
(
codeparam
,
'initial_display'
,
''
)
self
.
initial_display
=
find_with_default
(
codeparam
,
'initial_display'
,
''
)
self
.
answer
=
find_with_default
(
codeparam
,
'answer_display'
,
'No answer provided.'
)
...
...
@@ -1304,8 +1347,10 @@ class CodeResponse(LoncapaResponse):
else
:
# no <answer> stanza; get code from <script>
code
=
self
.
context
[
'script_code'
]
if
not
code
:
msg
=
'
%
s: Missing answer script code for coderesponse'
%
unicode
(
self
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
msg
=
'
%
s: Missing answer script code for coderesponse'
%
unicode
(
self
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
tests
=
self
.
xml
.
get
(
'tests'
)
...
...
@@ -1320,7 +1365,8 @@ class CodeResponse(LoncapaResponse):
try
:
exec
(
code
,
penv
,
penv
)
except
Exception
as
err
:
log
.
error
(
'Error in CodeResponse
%
s: Error in problem reference code'
%
err
)
log
.
error
(
'Error in CodeResponse
%
s: Error in problem reference code'
%
err
)
raise
Exception
(
err
)
try
:
self
.
answer
=
penv
[
'answer'
]
...
...
@@ -1333,7 +1379,7 @@ class CodeResponse(LoncapaResponse):
# Finally, make the ExternalResponse input XML format conform to the generic
# exteral grader interface
# The XML tagging of grader_payload is pyxserver-specific
grader_payload
=
'<pyxserver>'
grader_payload
=
'<pyxserver>'
grader_payload
+=
'<tests>'
+
tests
+
'</tests>
\n
'
grader_payload
+=
'<processor>'
+
code
+
'</processor>'
grader_payload
+=
'</pyxserver>'
...
...
@@ -1346,14 +1392,14 @@ 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
)))
(
err
,
self
.
answer_id
,
convert_files_to_filenames
(
student_answers
)))
raise
Exception
(
err
)
# We do not support xqueue within Studio.
if
self
.
system
.
xqueue
is
None
:
cmap
=
CorrectMap
()
cmap
.
set
(
self
.
answer_id
,
queuestate
=
None
,
msg
=
'Error checking problem: no external queueing server is configured.'
)
msg
=
'Error checking problem: no external queueing server is configured.'
)
return
cmap
# Prepare xqueue request
...
...
@@ -1368,9 +1414,11 @@ class CodeResponse(LoncapaResponse):
queuekey
=
xqueue_interface
.
make_hashkey
(
str
(
self
.
system
.
seed
)
+
qtime
+
anonymous_student_id
+
self
.
answer_id
)
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
self
.
system
.
xqueue
[
'callback_url'
],
lms_key
=
queuekey
,
queue_name
=
self
.
queue_name
)
callback_url
=
self
.
system
.
xqueue
[
'construct_callback'
]()
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
callback_url
,
lms_key
=
queuekey
,
queue_name
=
self
.
queue_name
)
# Generate body
if
is_list_of_files
(
submission
):
...
...
@@ -1381,13 +1429,16 @@ class CodeResponse(LoncapaResponse):
contents
=
self
.
payload
.
copy
()
# Metadata related to the student submission revealed to the external grader
# Metadata related to the student submission revealed to the external
# grader
student_info
=
{
'anonymous_student_id'
:
anonymous_student_id
,
'submission_time'
:
qtime
,
}
}
contents
.
update
({
'student_info'
:
json
.
dumps
(
student_info
)})
# Submit request. When successful, 'msg' is the prior length of the queue
# Submit request. When successful, 'msg' is the prior length of the
# queue
if
is_list_of_files
(
submission
):
# TODO: Is there any information we want to send here?
contents
.
update
({
'student_response'
:
''
})
...
...
@@ -1415,13 +1466,15 @@ class CodeResponse(LoncapaResponse):
# 2) Frontend: correctness='incomplete' eventually trickles down
# through inputtypes.textbox and .filesubmission to inform the
# browser to poll the LMS
cmap
.
set
(
self
.
answer_id
,
queuestate
=
queuestate
,
correctness
=
'incomplete'
,
msg
=
msg
)
cmap
.
set
(
self
.
answer_id
,
queuestate
=
queuestate
,
correctness
=
'incomplete'
,
msg
=
msg
)
return
cmap
def
update_score
(
self
,
score_msg
,
oldcmap
,
queuekey
):
(
valid_score_msg
,
correct
,
points
,
msg
)
=
self
.
_parse_score_msg
(
score_msg
)
(
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.'
)
...
...
@@ -1433,14 +1486,16 @@ class CodeResponse(LoncapaResponse):
self
.
context
[
'correct'
]
=
correctness
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
# does not match, we keep waiting for the score_msg whose key actually matches
# does not match, we keep waiting for the score_msg whose key actually
# matches
if
oldcmap
.
is_right_queuekey
(
self
.
answer_id
,
queuekey
):
# Sanity check on returned points
if
points
<
0
:
points
=
0
# Queuestate is consumed
oldcmap
.
set
(
self
.
answer_id
,
npoints
=
points
,
correctness
=
correctness
,
msg
=
msg
.
replace
(
' '
,
' '
),
queuestate
=
None
)
oldcmap
.
set
(
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
))
...
...
@@ -1560,15 +1615,18 @@ main()
if
answer
is
not
None
:
answer_src
=
answer
.
get
(
'src'
)
if
answer_src
is
not
None
:
self
.
code
=
self
.
system
.
filesystem
.
open
(
'src/'
+
answer_src
)
.
read
()
self
.
code
=
self
.
system
.
filesystem
.
open
(
'src/'
+
answer_src
)
.
read
()
else
:
self
.
code
=
answer
.
text
else
:
# no <answer> stanza; get code from <script>
self
.
code
=
self
.
context
[
'script_code'
]
if
not
self
.
code
:
msg
=
'
%
s: Missing answer script code for externalresponse'
%
unicode
(
self
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
msg
=
'
%
s: Missing answer script code for externalresponse'
%
unicode
(
self
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
self
.
tests
=
xml
.
get
(
'tests'
)
...
...
@@ -1591,10 +1649,12 @@ main()
payload
.
update
(
extra_payload
)
try
:
# call external server. TODO: synchronous call, can block for a long time
# call external server. TODO: synchronous call, can block for a
# long time
r
=
requests
.
post
(
self
.
url
,
data
=
payload
)
except
Exception
as
err
:
msg
=
'Error
%
s - cannot connect to external server url=
%
s'
%
(
err
,
self
.
url
)
msg
=
'Error
%
s - cannot connect to external server url=
%
s'
%
(
err
,
self
.
url
)
log
.
error
(
msg
)
raise
Exception
(
msg
)
...
...
@@ -1602,13 +1662,15 @@ main()
log
.
info
(
'response =
%
s'
%
r
.
text
)
if
(
not
r
.
text
)
or
(
not
r
.
text
.
strip
()):
raise
Exception
(
'Error: no response from external server url=
%
s'
%
self
.
url
)
raise
Exception
(
'Error: no response from external server url=
%
s'
%
self
.
url
)
try
:
# response is XML; parse it
rxml
=
etree
.
fromstring
(
r
.
text
)
except
Exception
as
err
:
msg
=
'Error
%
s - cannot parse response from external server r.text=
%
s'
%
(
err
,
r
.
text
)
msg
=
'Error
%
s - cannot parse response from external server r.text=
%
s'
%
(
err
,
r
.
text
)
log
.
error
(
msg
)
raise
Exception
(
msg
)
...
...
@@ -1633,7 +1695,8 @@ main()
except
Exception
as
err
:
log
.
error
(
'Error
%
s'
%
err
)
if
self
.
system
.
DEBUG
:
cmap
.
set_dict
(
dict
(
zip
(
sorted
(
self
.
answer_ids
),
[
'incorrect'
]
*
len
(
idset
))))
cmap
.
set_dict
(
dict
(
zip
(
sorted
(
self
.
answer_ids
),
[
'incorrect'
]
*
len
(
idset
))))
cmap
.
set_property
(
self
.
answer_ids
[
0
],
'msg'
,
'<span class="inline-error">
%
s</span>'
%
str
(
err
)
.
replace
(
'<'
,
'<'
))
...
...
@@ -1650,7 +1713,8 @@ main()
# create CorrectMap
for
key
in
idset
:
idx
=
idset
.
index
(
key
)
msg
=
rxml
.
find
(
'message'
)
.
text
.
replace
(
' '
,
' '
)
if
idx
==
0
else
None
msg
=
rxml
.
find
(
'message'
)
.
text
.
replace
(
' '
,
' '
)
if
idx
==
0
else
None
cmap
.
set
(
key
,
self
.
context
[
'correct'
][
idx
],
msg
=
msg
)
return
cmap
...
...
@@ -1665,7 +1729,8 @@ main()
except
Exception
as
err
:
log
.
error
(
'Error
%
s'
%
err
)
if
self
.
system
.
DEBUG
:
msg
=
'<span class="inline-error">
%
s</span>'
%
str
(
err
)
.
replace
(
'<'
,
'<'
)
msg
=
'<span class="inline-error">
%
s</span>'
%
str
(
err
)
.
replace
(
'<'
,
'<'
)
exans
=
[
''
]
*
len
(
self
.
answer_ids
)
exans
[
0
]
=
msg
...
...
@@ -1712,8 +1777,9 @@ class FormulaResponse(LoncapaResponse):
self
.
correct_answer
=
contextualize_text
(
xml
.
get
(
'answer'
),
context
)
self
.
samples
=
contextualize_text
(
xml
.
get
(
'samples'
),
context
)
try
:
self
.
tolerance_xml
=
xml
.
xpath
(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
id
=
xml
.
get
(
'id'
))[
0
]
self
.
tolerance_xml
=
xml
.
xpath
(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
id
=
xml
.
get
(
'id'
))[
0
]
self
.
tolerance
=
contextualize_text
(
self
.
tolerance_xml
,
context
)
except
Exception
:
self
.
tolerance
=
'0.00001'
...
...
@@ -1735,14 +1801,15 @@ 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
)
correctness
=
self
.
check_formula
(
self
.
correct_answer
,
given
,
self
.
samples
)
return
CorrectMap
(
self
.
answer_id
,
correctness
)
def
check_formula
(
self
,
expected
,
given
,
samples
):
variables
=
samples
.
split
(
'@'
)[
0
]
.
split
(
','
)
numsamples
=
int
(
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
1
])
sranges
=
zip
(
*
map
(
lambda
x
:
map
(
float
,
x
.
split
(
","
)),
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
0
]
.
split
(
':'
)))
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
0
]
.
split
(
':'
)))
ranges
=
dict
(
zip
(
variables
,
sranges
))
for
i
in
range
(
numsamples
):
...
...
@@ -1753,22 +1820,26 @@ class FormulaResponse(LoncapaResponse):
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))
# log.debug('formula: instructor_vars=%s, expected=%s' %
# (instructor_variables,expected))
instructor_result
=
evaluator
(
instructor_variables
,
dict
(),
expected
,
cs
=
self
.
case_sensitive
)
try
:
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
# log.debug('formula: student_vars=%s, given=%s' %
# (student_variables,given))
student_result
=
evaluator
(
student_variables
,
dict
(),
given
,
cs
=
self
.
case_sensitive
)
except
UndefinedVariable
as
uv
:
log
.
debug
(
'formularesponse: undefined variable in given=
%
s'
%
given
)
raise
StudentInputError
(
"Invalid input: "
+
uv
.
message
+
" not permitted in answer"
)
log
.
debug
(
'formularesponse: undefined variable in given=
%
s'
%
given
)
raise
StudentInputError
(
"Invalid input: "
+
uv
.
message
+
" not permitted in answer"
)
except
Exception
as
err
:
#traceback.print_exc()
#
traceback.print_exc()
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
))
if
numpy
.
isnan
(
student_result
)
or
numpy
.
isinf
(
student_result
):
return
"incorrect"
...
...
@@ -1792,9 +1863,11 @@ class FormulaResponse(LoncapaResponse):
for
hxml
in
hxml_set
:
samples
=
hxml
.
get
(
'samples'
)
name
=
hxml
.
get
(
'name'
)
correct_answer
=
contextualize_text
(
hxml
.
get
(
'answer'
),
self
.
context
)
correct_answer
=
contextualize_text
(
hxml
.
get
(
'answer'
),
self
.
context
)
try
:
correctness
=
self
.
check_formula
(
correct_answer
,
given
,
samples
)
correctness
=
self
.
check_formula
(
correct_answer
,
given
,
samples
)
except
Exception
:
correctness
=
'incorrect'
if
correctness
==
'correct'
:
...
...
@@ -1825,11 +1898,13 @@ class SchematicResponse(LoncapaResponse):
def
get_score
(
self
,
student_answers
):
from
capa_problem
import
global_context
submission
=
[
json
.
loads
(
student_answers
[
k
])
for
k
in
sorted
(
self
.
answer_ids
)]
submission
=
[
json
.
loads
(
student_answers
[
k
])
for
k
in
sorted
(
self
.
answer_ids
)]
self
.
context
.
update
({
'submission'
:
submission
})
exec
self
.
code
in
global_context
,
self
.
context
cmap
=
CorrectMap
()
cmap
.
set_dict
(
dict
(
zip
(
sorted
(
self
.
answer_ids
),
self
.
context
[
'correct'
])))
cmap
.
set_dict
(
dict
(
zip
(
sorted
(
self
.
answer_ids
),
self
.
context
[
'correct'
])))
return
cmap
def
get_answers
(
self
):
...
...
@@ -1891,12 +1966,14 @@ class ImageResponse(LoncapaResponse):
expectedset
=
self
.
get_answers
()
for
aid
in
self
.
answer_ids
:
# loop through IDs of <imageinput>
# fields in our stanza
given
=
student_answers
[
aid
]
# this should be a string of the form '[x,y]'
given
=
student_answers
[
aid
]
# this should be a string of the form '[x,y]'
correct_map
.
set
(
aid
,
'incorrect'
)
if
not
given
:
# No answer to parse. Mark as incorrect and move on
continue
# parse given answer
m
=
re
.
match
(
'
\
[([0-9]+),([0-9]+)]'
,
given
.
strip
()
.
replace
(
' '
,
''
))
m
=
re
.
match
(
'
\
[([0-9]+),([0-9]+)]'
,
given
.
strip
()
.
replace
(
' '
,
''
))
if
not
m
:
raise
Exception
(
'[capamodule.capa.responsetypes.imageinput] '
'error grading
%
s (input=
%
s)'
%
(
aid
,
given
))
...
...
@@ -1904,20 +1981,24 @@ class ImageResponse(LoncapaResponse):
rectangles
,
regions
=
expectedset
if
rectangles
[
aid
]:
# rectangles part - for backward compatibility
# Check whether given point lies in any of the solution rectangles
# Check whether given point lies in any of the solution
# rectangles
solution_rectangles
=
rectangles
[
aid
]
.
split
(
';'
)
for
solution_rectangle
in
solution_rectangles
:
# parse expected answer
# TODO: Compile regexp on file load
m
=
re
.
match
(
'[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]-[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]'
,
solution_rectangle
.
strip
()
.
replace
(
' '
,
''
))
m
=
re
.
match
(
'[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]-[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]'
,
solution_rectangle
.
strip
()
.
replace
(
' '
,
''
))
if
not
m
:
msg
=
'Error in problem specification! cannot parse rectangle in
%
s'
%
(
etree
.
tostring
(
self
.
ielements
[
aid
],
pretty_print
=
True
))
raise
Exception
(
'[capamodule.capa.responsetypes.imageinput] '
+
msg
)
raise
Exception
(
'[capamodule.capa.responsetypes.imageinput] '
+
msg
)
(
llx
,
lly
,
urx
,
ury
)
=
[
int
(
x
)
for
x
in
m
.
groups
()]
# answer is correct if (x,y) is within the specified rectangle
# answer is correct if (x,y) is within the specified
# rectangle
if
(
llx
<=
gx
<=
urx
)
and
(
lly
<=
gy
<=
ury
):
correct_map
.
set
(
aid
,
'correct'
)
break
...
...
@@ -1938,10 +2019,13 @@ class ImageResponse(LoncapaResponse):
return
correct_map
def
get_answers
(
self
):
return
(
dict
([(
ie
.
get
(
'id'
),
ie
.
get
(
'rectangle'
))
for
ie
in
self
.
ielements
]),
dict
([(
ie
.
get
(
'id'
),
ie
.
get
(
'regions'
))
for
ie
in
self
.
ielements
]))
return
(
dict
([(
ie
.
get
(
'id'
),
ie
.
get
(
'rectangle'
))
for
ie
in
self
.
ielements
]),
dict
([(
ie
.
get
(
'id'
),
ie
.
get
(
'regions'
))
for
ie
in
self
.
ielements
]))
#-----------------------------------------------------------------------------
class
AnnotationResponse
(
LoncapaResponse
):
'''
Checking of annotation responses.
...
...
@@ -1952,7 +2036,8 @@ class AnnotationResponse(LoncapaResponse):
response_tag
=
'annotationresponse'
allowed_inputfields
=
[
'annotationinput'
]
max_inputfields
=
1
default_scoring
=
{
'incorrect'
:
0
,
'partially-correct'
:
1
,
'correct'
:
2
}
default_scoring
=
{
'incorrect'
:
0
,
'partially-correct'
:
1
,
'correct'
:
2
}
def
setup_response
(
self
):
xml
=
self
.
xml
self
.
scoring_map
=
self
.
_get_scoring_map
()
...
...
@@ -1966,7 +2051,8 @@ class AnnotationResponse(LoncapaResponse):
student_option
=
self
.
_get_submitted_option_id
(
student_answer
)
scoring
=
self
.
scoring_map
[
self
.
answer_id
]
is_valid
=
student_option
is
not
None
and
student_option
in
scoring
.
keys
()
is_valid
=
student_option
is
not
None
and
student_option
in
scoring
.
keys
(
)
(
correctness
,
points
)
=
(
'incorrect'
,
None
)
if
is_valid
:
...
...
@@ -1981,7 +2067,7 @@ class AnnotationResponse(LoncapaResponse):
def
_get_scoring_map
(
self
):
''' Returns a dict of option->scoring for each input. '''
scoring
=
self
.
default_scoring
choices
=
dict
([(
choice
,
choice
)
for
choice
in
scoring
])
choices
=
dict
([(
choice
,
choice
)
for
choice
in
scoring
])
scoring_map
=
{}
for
inputfield
in
self
.
inputfields
:
...
...
@@ -1998,9 +2084,11 @@ class AnnotationResponse(LoncapaResponse):
''' Returns a dict of answers for each input.'''
answer_map
=
{}
for
inputfield
in
self
.
inputfields
:
correct_option
=
self
.
_find_option_with_choice
(
inputfield
,
'correct'
)
correct_option
=
self
.
_find_option_with_choice
(
inputfield
,
'correct'
)
if
correct_option
is
not
None
:
answer_map
[
inputfield
.
get
(
'id'
)]
=
correct_option
.
get
(
'description'
)
answer_map
[
inputfield
.
get
(
'id'
)]
=
correct_option
.
get
(
'description'
)
return
answer_map
def
_get_max_points
(
self
):
...
...
@@ -2016,7 +2104,7 @@ class AnnotationResponse(LoncapaResponse):
'id'
:
index
,
'description'
:
option
.
text
,
'choice'
:
option
.
get
(
'choice'
)
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
def
_find_option_with_choice
(
self
,
inputfield
,
choice
):
''' Returns the option with the given choice value, otherwise None. '''
...
...
common/lib/capa/capa/templates/matlabinput.html
0 → 100644
View file @
f0e1b477
<section
id=
"textbox_${id}"
class=
"textbox"
>
<textarea
rows=
"${rows}"
cols=
"${cols}"
name=
"input_${id}"
id=
"input_${id}"
%
if
hidden:
style=
"display:none;"
%
endif
>
${value|h}
</textarea>
<div
class=
"grader-status"
>
% if status == 'unsubmitted':
<span
class=
"unanswered"
style=
"display:inline-block;"
id=
"status_${id}"
>
Unanswered
</span>
% elif status == 'correct':
<span
class=
"correct"
id=
"status_${id}"
>
Correct
</span>
% elif status == 'incorrect':
<span
class=
"incorrect"
id=
"status_${id}"
>
Incorrect
</span>
% elif status == 'queued':
<span
class=
"processing"
id=
"status_${id}"
>
Queued
</span>
<span
style=
"display:none;"
class=
"xqueue"
id=
"${id}"
>
${queue_len}
</span>
% endif
% if hidden:
<div
style=
"display:none;"
name=
"${hidden}"
inputid=
"input_${id}"
/>
% endif
<p
class=
"debug"
>
${status}
</p>
</div>
<span
id=
"answer_${id}"
></span>
<div
class=
"external-grader-message"
>
${msg|n}
</div>
<div
class=
"external-grader-message"
>
${queue_msg|n}
</div>
<div
class=
"plot-button"
>
<input
type=
"button"
class=
"save"
name=
"plot-button"
id=
"plot_${id}"
value=
"Plot"
/>
</div>
<script>
// Note: We need to make the area follow the CodeMirror for this to work.
$
(
function
(){
var
cm
=
CodeMirror
.
fromTextArea
(
document
.
getElementById
(
"input_${id}"
),
{
%
if
linenumbers
==
'true'
:
lineNumbers
:
true
,
%
endif
mode
:
"matlab"
,
matchBrackets
:
true
,
lineWrapping
:
true
,
indentUnit
:
"${tabsize}"
,
tabSize
:
"${tabsize}"
,
indentWithTabs
:
false
,
extraKeys
:
{
"Tab"
:
function
(
cm
)
{
cm
.
replaceSelection
(
"${' '*tabsize}"
,
"end"
);
}
},
smartIndent
:
false
});
$
(
"#textbox_${id}"
).
find
(
'.CodeMirror-scroll'
).
height
(
$
{
int
(
13.5
*
eval
(
rows
))});
var
gentle_alert
=
function
(
parent_elt
,
msg
)
{
if
(
$
(
parent_elt
).
find
(
'.capa_alert'
).
length
)
{
$
(
parent_elt
).
find
(
'.capa_alert'
).
remove
();
}
var
alert_elem
=
"<div>"
+
msg
+
"</div>"
;
alert_elem
=
$
(
alert_elem
).
addClass
(
'capa_alert'
);
$
(
parent_elt
).
find
(
'.action'
).
after
(
alert_elem
);
$
(
parent_elt
).
find
(
'.capa_alert'
).
css
({
opacity
:
0
}).
animate
({
opacity
:
1
},
700
);
}
// hook up the plot button
var
plot
=
function
(
event
)
{
var
problem_elt
=
$
(
event
.
target
).
closest
(
'.problems-wrapper'
);
url
=
$
(
event
.
target
).
closest
(
'.problems-wrapper'
).
data
(
'url'
);
input_id
=
"${id}"
;
// save the codemirror text to the textarea
cm
.
save
();
var
input
=
$
(
"#input_${id}"
);
// pull out the coded text
submission
=
input
.
val
();
answer
=
input
.
serialize
();
// setup callback for after we send information to plot
var
plot_callback
=
function
(
response
)
{
if
(
response
.
success
)
{
window
.
location
.
reload
();
}
else
{
gentle_alert
(
problem_elt
,
msg
);
}
}
var
save_callback
=
function
(
response
)
{
if
(
response
.
success
)
{
// send information to the problem's plot functionality
Problem
.
inputAjax
(
url
,
input_id
,
'plot'
,
{
'submission'
:
submission
},
plot_callback
);
}
else
{
gentle_alert
(
problem_elt
,
msg
);
}
}
// save the answer
$
.
postWithPrefix
(
url
+
'/problem_save'
,
answer
,
save_callback
);
}
$
(
'#plot_${id}'
).
click
(
plot
);
});
</script>
</section>
common/lib/capa/capa/tests/__init__.py
View file @
f0e1b477
...
...
@@ -2,7 +2,7 @@ import fs
import
fs.osfs
import
os
from
mock
import
Mock
from
mock
import
Mock
,
MagicMock
import
xml.sax.saxutils
as
saxutils
...
...
@@ -16,6 +16,11 @@ def tst_render_template(template, context):
"""
return
'<div>{0}</div>'
.
format
(
saxutils
.
escape
(
repr
(
context
)))
def
calledback_url
(
dispatch
=
'score_update'
):
return
dispatch
xqueue_interface
=
MagicMock
()
xqueue_interface
.
send_to_queue
.
return_value
=
(
0
,
'Success!'
)
test_system
=
Mock
(
ajax_url
=
'courses/course_id/modx/a_location'
,
...
...
@@ -26,7 +31,7 @@ test_system = Mock(
user
=
Mock
(),
filestore
=
fs
.
osfs
.
OSFS
(
os
.
path
.
join
(
TEST_DIR
,
"test_files"
)),
debug
=
True
,
xqueue
=
{
'interface'
:
None
,
'callback_url'
:
'/'
,
'default_queuename'
:
'testqueue'
,
'waittime'
:
10
},
xqueue
=
{
'interface'
:
xqueue_interface
,
'construct_callback'
:
calledback_url
,
'default_queuename'
:
'testqueue'
,
'waittime'
:
10
},
node_path
=
os
.
environ
.
get
(
"NODE_PATH"
,
"/usr/local/lib/node_modules"
),
anonymous_student_id
=
'student'
)
common/lib/capa/capa/tests/test_inputtypes.py
View file @
f0e1b477
...
...
@@ -23,6 +23,7 @@ import xml.sax.saxutils as saxutils
from
.
import
test_system
from
capa
import
inputtypes
from
mock
import
ANY
# just a handy shortcut
lookup_tag
=
inputtypes
.
registry
.
get_class_for_tag
...
...
@@ -300,6 +301,98 @@ class CodeInputTest(unittest.TestCase):
self
.
assertEqual
(
context
,
expected
)
class
MatlabTest
(
unittest
.
TestCase
):
'''
Test Matlab input types
'''
def
setUp
(
self
):
self
.
rows
=
'10'
self
.
cols
=
'80'
self
.
tabsize
=
'4'
self
.
mode
=
""
self
.
payload
=
"payload"
self
.
linenumbers
=
'true'
self
.
xml
=
"""<matlabinput id="prob_1_2"
rows="{r}" cols="{c}"
tabsize="{tabsize}" mode="{m}"
linenumbers="{ln}">
<plot_payload>
{payload}
</plot_payload>
</matlabinput>"""
.
format
(
r
=
self
.
rows
,
c
=
self
.
cols
,
tabsize
=
self
.
tabsize
,
m
=
self
.
mode
,
payload
=
self
.
payload
,
ln
=
self
.
linenumbers
)
elt
=
etree
.
fromstring
(
self
.
xml
)
state
=
{
'value'
:
'print "good evening"'
,
'status'
:
'incomplete'
,
'feedback'
:
{
'message'
:
'3'
},
}
self
.
input_class
=
lookup_tag
(
'matlabinput'
)
self
.
the_input
=
self
.
input_class
(
test_system
,
elt
,
state
)
def
test_rendering
(
self
):
context
=
self
.
the_input
.
_get_render_context
()
expected
=
{
'id'
:
'prob_1_2'
,
'value'
:
'print "good evening"'
,
'status'
:
'queued'
,
'msg'
:
self
.
input_class
.
submitted_msg
,
'mode'
:
self
.
mode
,
'rows'
:
self
.
rows
,
'cols'
:
self
.
cols
,
'queue_msg'
:
''
,
'linenumbers'
:
'true'
,
'hidden'
:
''
,
'tabsize'
:
int
(
self
.
tabsize
),
'queue_len'
:
'3'
,
}
self
.
assertEqual
(
context
,
expected
)
def
test_rendering_with_state
(
self
):
state
=
{
'value'
:
'print "good evening"'
,
'status'
:
'incomplete'
,
'input_state'
:
{
'queue_msg'
:
'message'
},
'feedback'
:
{
'message'
:
'3'
},
}
elt
=
etree
.
fromstring
(
self
.
xml
)
input_class
=
lookup_tag
(
'matlabinput'
)
the_input
=
self
.
input_class
(
test_system
,
elt
,
state
)
context
=
the_input
.
_get_render_context
()
expected
=
{
'id'
:
'prob_1_2'
,
'value'
:
'print "good evening"'
,
'status'
:
'queued'
,
'msg'
:
self
.
input_class
.
submitted_msg
,
'mode'
:
self
.
mode
,
'rows'
:
self
.
rows
,
'cols'
:
self
.
cols
,
'queue_msg'
:
'message'
,
'linenumbers'
:
'true'
,
'hidden'
:
''
,
'tabsize'
:
int
(
self
.
tabsize
),
'queue_len'
:
'3'
,
}
self
.
assertEqual
(
context
,
expected
)
def
test_plot_data
(
self
):
get
=
{
'submission'
:
'x = 1234;'
}
response
=
self
.
the_input
.
handle_ajax
(
"plot"
,
get
)
test_system
.
xqueue
[
'interface'
]
.
send_to_queue
.
assert_called_with
(
header
=
ANY
,
body
=
ANY
)
self
.
assertTrue
(
response
[
'success'
])
self
.
assertTrue
(
self
.
the_input
.
input_state
[
'queuekey'
]
is
not
None
)
self
.
assertEqual
(
self
.
the_input
.
input_state
[
'queuestate'
],
'queued'
)
class
SchematicTest
(
unittest
.
TestCase
):
'''
...
...
common/lib/xmodule/xmodule/capa_module.py
View file @
f0e1b477
...
...
@@ -93,6 +93,7 @@ class CapaFields(object):
rerandomize
=
Randomization
(
help
=
"When to rerandomize the problem"
,
default
=
"always"
,
scope
=
Scope
.
settings
)
data
=
String
(
help
=
"XML data for the problem"
,
scope
=
Scope
.
content
)
correct_map
=
Object
(
help
=
"Dictionary with the correctness of current student answers"
,
scope
=
Scope
.
student_state
,
default
=
{})
input_state
=
Object
(
help
=
"Dictionary for maintaining the state of inputtypes"
,
scope
=
Scope
.
student_state
,
default
=
{})
student_answers
=
Object
(
help
=
"Dictionary with the current student responses"
,
scope
=
Scope
.
student_state
)
done
=
Boolean
(
help
=
"Whether the student has answered the problem"
,
scope
=
Scope
.
student_state
)
display_name
=
String
(
help
=
"Display name for this module"
,
scope
=
Scope
.
settings
)
...
...
@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
'done'
:
self
.
done
,
'correct_map'
:
self
.
correct_map
,
'student_answers'
:
self
.
student_answers
,
'input_state'
:
self
.
input_state
,
'seed'
:
self
.
seed
,
}
...
...
@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
lcp_state
=
self
.
lcp
.
get_state
()
self
.
done
=
lcp_state
[
'done'
]
self
.
correct_map
=
lcp_state
[
'correct_map'
]
self
.
input_state
=
lcp_state
[
'input_state'
]
self
.
student_answers
=
lcp_state
[
'student_answers'
]
self
.
seed
=
lcp_state
[
'seed'
]
...
...
@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
'problem_save'
:
self
.
save_problem
,
'problem_show'
:
self
.
get_answer
,
'score_update'
:
self
.
update_score
,
'input_ajax'
:
self
.
lcp
.
handle_input_ajax
'input_ajax'
:
self
.
handle_input_ajax
,
'ungraded_response'
:
self
.
handle_ungraded_response
}
if
dispatch
not
in
handlers
:
...
...
@@ -537,6 +541,43 @@ class CapaModule(CapaFields, XModule):
return
dict
()
# No AJAX return is needed
def
handle_ungraded_response
(
self
,
get
):
'''
Delivers a response from the XQueue to the capa problem
The score of the problem will not be updated
Args:
- get (dict) must contain keys:
queuekey - a key specific to this response
xqueue_body - the body of the response
Returns:
empty dictionary
No ajax return is needed, so an empty dict is returned
'''
queuekey
=
get
[
'queuekey'
]
score_msg
=
get
[
'xqueue_body'
]
# pass along the xqueue message to the problem
self
.
lcp
.
ungraded_response
(
score_msg
,
queuekey
)
self
.
set_state_from_lcp
()
return
dict
()
def
handle_input_ajax
(
self
,
get
):
'''
Handle ajax calls meant for a particular input in the problem
Args:
- get (dict) - data that should be passed to the input
Returns:
- dict containing the response from the input
'''
response
=
self
.
lcp
.
handle_input_ajax
(
get
)
# save any state changes that may occur
self
.
set_state_from_lcp
()
return
response
def
get_answer
(
self
,
get
):
'''
For the "show answer" button.
...
...
common/lib/xmodule/xmodule/js/src/capa/display.coffee
View file @
f0e1b477
...
...
@@ -41,6 +41,11 @@ class @Problem
@
el
.
attr
progress
:
response
.
progress_status
@
el
.
trigger
(
'progressChanged'
)
forceUpdate
:
(
response
)
=>
@
el
.
attr
progress
:
response
.
progress_status
@
el
.
trigger
(
'progressChanged'
)
queueing
:
=>
@
queued_items
=
@
$
(
".xqueue"
)
@
num_queued_items
=
@
queued_items
.
length
...
...
@@ -71,6 +76,7 @@ class @Problem
@
num_queued_items
=
@
new_queued_items
.
length
if
@
num_queued_items
==
0
@
forceUpdate
response
delete
window
.
queuePollerID
else
# TODO: Some logic to dynamically adjust polling rate based on queuelen
...
...
common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
View file @
f0e1b477
...
...
@@ -174,7 +174,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
str
(
len
(
self
.
child_history
)))
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
system
.
xqueue
[
'c
allback_url'
]
,
lms_callback_url
=
system
.
xqueue
[
'c
onstruct_callback'
]()
,
lms_key
=
queuekey
,
queue_name
=
self
.
message_queue_name
)
...
...
@@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
anonymous_student_id
+
str
(
len
(
self
.
child_history
)))
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
system
.
xqueue
[
'c
allback_url'
]
,
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
system
.
xqueue
[
'c
onstruct_callback'
]()
,
lms_key
=
queuekey
,
queue_name
=
self
.
queue_name
)
...
...
common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
View file @
f0e1b477
...
...
@@ -183,7 +183,10 @@ class OpenEndedModuleTest(unittest.TestCase):
self
.
test_system
.
location
=
self
.
location
self
.
mock_xqueue
=
MagicMock
()
self
.
mock_xqueue
.
send_to_queue
.
return_value
=
(
None
,
"Message"
)
self
.
test_system
.
xqueue
=
{
'interface'
:
self
.
mock_xqueue
,
'callback_url'
:
'/'
,
'default_queuename'
:
'testqueue'
,
def
constructed_callback
(
dispatch
=
"score_update"
):
return
dispatch
self
.
test_system
.
xqueue
=
{
'interface'
:
self
.
mock_xqueue
,
'construct_callback'
:
constructed_callback
,
'default_queuename'
:
'testqueue'
,
'waittime'
:
1
}
self
.
openendedmodule
=
OpenEndedModule
(
self
.
test_system
,
self
.
location
,
self
.
definition
,
self
.
descriptor
,
self
.
static_data
,
self
.
metadata
)
...
...
lms/djangoapps/courseware/module_render.py
View file @
f0e1b477
...
...
@@ -181,12 +181,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
host
=
request
.
get_host
(),
proto
=
request
.
META
.
get
(
'HTTP_X_FORWARDED_PROTO'
,
'https'
if
request
.
is_secure
()
else
'http'
)
)
xqueue_callback_url
+=
reverse
(
'xqueue_callback'
,
kwargs
=
dict
(
course_id
=
course_id
,
userid
=
str
(
user
.
id
),
id
=
descriptor
.
location
.
url
(),
dispatch
=
'score_update'
),
)
def
make_xqueue_callback
(
dispatch
=
'score_update'
):
# Fully qualified callback URL for external queueing system
xqueue_callback_url
=
'{proto}://{host}'
.
format
(
host
=
request
.
get_host
(),
proto
=
request
.
META
.
get
(
'HTTP_X_FORWARDED_PROTO'
,
'https'
if
request
.
is_secure
()
else
'http'
)
)
xqueue_callback_url
+=
reverse
(
'xqueue_callback'
,
kwargs
=
dict
(
course_id
=
course_id
,
userid
=
str
(
user
.
id
),
id
=
descriptor
.
location
.
url
(),
dispatch
=
dispatch
),
)
return
xqueue_callback_url
# Default queuename is course-specific and is derived from the course that
# contains the current module.
...
...
@@ -194,7 +203,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
xqueue_default_queuename
=
descriptor
.
location
.
org
+
'-'
+
descriptor
.
location
.
course
xqueue
=
{
'interface'
:
xqueue_interface
,
'c
allback_url'
:
xqueue_callback_url
,
'c
onstruct_callback'
:
make_xqueue_callback
,
'default_queuename'
:
xqueue_default_queuename
.
replace
(
' '
,
'_'
),
'waittime'
:
settings
.
XQUEUE_WAITTIME_BETWEEN_REQUESTS
}
...
...
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