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.
...
@@ -16,7 +16,6 @@ This is used by capa_module.
from
__future__
import
division
from
__future__
import
division
from
datetime
import
datetime
from
datetime
import
datetime
import
json
import
logging
import
logging
import
math
import
math
import
numpy
import
numpy
...
@@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape
...
@@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape
from
copy
import
deepcopy
from
copy
import
deepcopy
import
chem
import
chem
import
chem.chemcalc
import
chem.chemtools
import
chem.miller
import
chem.miller
import
verifiers
import
verifiers
import
verifiers.draganddrop
import
verifiers.draganddrop
...
@@ -70,9 +67,6 @@ global_context = {'random': random,
...
@@ -70,9 +67,6 @@ global_context = {'random': random,
'scipy'
:
scipy
,
'scipy'
:
scipy
,
'calc'
:
calc
,
'calc'
:
calc
,
'eia'
:
eia
,
'eia'
:
eia
,
'chemcalc'
:
chem
.
chemcalc
,
'chemtools'
:
chem
.
chemtools
,
'miller'
:
chem
.
miller
,
'draganddrop'
:
verifiers
.
draganddrop
}
'draganddrop'
:
verifiers
.
draganddrop
}
# These should be removed from HTML output, including all subelements
# These should be removed from HTML output, including all subelements
...
@@ -97,8 +91,13 @@ class LoncapaProblem(object):
...
@@ -97,8 +91,13 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- 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,
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
rendering, and user context
...
@@ -110,21 +109,23 @@ class LoncapaProblem(object):
...
@@ -110,21 +109,23 @@ class LoncapaProblem(object):
self
.
system
=
system
self
.
system
=
system
if
self
.
system
is
None
:
if
self
.
system
is
None
:
raise
Exception
()
raise
Exception
()
self
.
seed
=
seed
state
=
state
if
state
else
{}
if
state
:
if
'seed'
in
state
:
# Set seed according to the following priority:
self
.
seed
=
state
[
'seed'
]
# 1. Contained in problem's state
if
'student_answers'
in
state
:
# 2. Passed into capa_problem via constructor
self
.
student_answers
=
state
[
'student_answers'
]
# 3. Assign from the OS's random number generator
if
'correct_map'
in
state
:
self
.
seed
=
state
.
get
(
'seed'
,
seed
)
self
.
correct_map
.
set_dict
(
state
[
'correct_map'
])
if
self
.
seed
is
None
:
if
'done'
in
state
:
self
.
seed
=
struct
.
unpack
(
'i'
,
os
.
urandom
(
4
))
self
.
done
=
state
[
'done'
]
self
.
student_answers
=
state
.
get
(
'student_answers'
,
{})
if
'correct_map'
in
state
:
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
self
.
correct_map
.
set_dict
(
state
[
'correct_map'
])
if
not
self
.
seed
:
self
.
done
=
state
.
get
(
'done'
,
False
)
self
.
seed
=
struct
.
unpack
(
'i'
,
os
.
urandom
(
4
))[
0
]
self
.
input_state
=
state
.
get
(
'input_state'
,
{})
# Convert startouttext and endouttext to proper <text></text>
# Convert startouttext and endouttext to proper <text></text>
problem_text
=
re
.
sub
(
"startouttext
\
s*/"
,
"text"
,
problem_text
)
problem_text
=
re
.
sub
(
"startouttext
\
s*/"
,
"text"
,
problem_text
)
...
@@ -188,6 +189,7 @@ class LoncapaProblem(object):
...
@@ -188,6 +189,7 @@ class LoncapaProblem(object):
return
{
'seed'
:
self
.
seed
,
return
{
'seed'
:
self
.
seed
,
'student_answers'
:
self
.
student_answers
,
'student_answers'
:
self
.
student_answers
,
'correct_map'
:
self
.
correct_map
.
get_dict
(),
'correct_map'
:
self
.
correct_map
.
get_dict
(),
'input_state'
:
self
.
input_state
,
'done'
:
self
.
done
}
'done'
:
self
.
done
}
def
get_max_score
(
self
):
def
get_max_score
(
self
):
...
@@ -237,6 +239,20 @@ class LoncapaProblem(object):
...
@@ -237,6 +239,20 @@ class LoncapaProblem(object):
self
.
correct_map
.
set_dict
(
cmap
.
get_dict
())
self
.
correct_map
.
set_dict
(
cmap
.
get_dict
())
return
cmap
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
):
def
is_queued
(
self
):
'''
'''
Returns True if any part of the problem has been submitted to an external queue
Returns True if any part of the problem has been submitted to an external queue
...
@@ -351,7 +367,7 @@ class LoncapaProblem(object):
...
@@ -351,7 +367,7 @@ class LoncapaProblem(object):
dispatch
=
get
[
'dispatch'
]
dispatch
=
get
[
'dispatch'
]
return
self
.
inputs
[
input_id
]
.
handle_ajax
(
dispatch
,
get
)
return
self
.
inputs
[
input_id
]
.
handle_ajax
(
dispatch
,
get
)
else
:
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
{}
return
{}
...
@@ -527,11 +543,15 @@ class LoncapaProblem(object):
...
@@ -527,11 +543,15 @@ class LoncapaProblem(object):
value
=
""
value
=
""
if
self
.
student_answers
and
problemid
in
self
.
student_answers
:
if
self
.
student_answers
and
problemid
in
self
.
student_answers
:
value
=
self
.
student_answers
[
problemid
]
value
=
self
.
student_answers
[
problemid
]
if
input_id
not
in
self
.
input_state
:
self
.
input_state
[
input_id
]
=
{}
# do the rendering
# do the rendering
state
=
{
'value'
:
value
,
state
=
{
'value'
:
value
,
'status'
:
status
,
'status'
:
status
,
'id'
:
input_id
,
'id'
:
input_id
,
'input_state'
:
self
.
input_state
[
input_id
],
'feedback'
:
{
'message'
:
msg
,
'feedback'
:
{
'message'
:
msg
,
'hint'
:
hint
,
'hint'
:
hint
,
'hintmode'
:
hintmode
,
}}
'hintmode'
:
hintmode
,
}}
...
...
common/lib/capa/capa/inputtypes.py
View file @
f0e1b477
...
@@ -37,18 +37,18 @@ graded status as'status'
...
@@ -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
# 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.
# general css and layout strategy for capa, document it, then implement it.
from
collections
import
namedtuple
import
json
import
json
import
logging
import
logging
from
lxml
import
etree
from
lxml
import
etree
import
re
import
re
import
shlex
# for splitting quoted strings
import
shlex
# for splitting quoted strings
import
sys
import
sys
import
os
import
pyparsing
import
pyparsing
from
.registry
import
TagRegistry
from
.registry
import
TagRegistry
from
capa.chem
import
chemcalc
from
capa.chem
import
chemcalc
import
xqueue_interface
from
datetime
import
datetime
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -97,7 +97,8 @@ class Attribute(object):
...
@@ -97,7 +97,8 @@ class Attribute(object):
"""
"""
val
=
element
.
get
(
self
.
name
)
val
=
element
.
get
(
self
.
name
)
if
self
.
default
==
self
.
_sentinel
and
val
is
None
:
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
:
if
val
is
None
:
# not required, so return default
# not required, so return default
...
@@ -132,6 +133,8 @@ class InputTypeBase(object):
...
@@ -132,6 +133,8 @@ class InputTypeBase(object):
* 'id' -- the id of this input, typically
* 'id' -- the id of this input, typically
"{problem-location}_{response-num}_{input-num}"
"{problem-location}_{response-num}_{input-num}"
* 'status' (answered, unanswered, unsubmitted)
* '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' (dictionary containing keys for hints, errors, or other
feedback from previous attempt. Specifically 'message', 'hint',
feedback from previous attempt. Specifically 'message', 'hint',
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
...
@@ -149,7 +152,8 @@ class InputTypeBase(object):
...
@@ -149,7 +152,8 @@ class InputTypeBase(object):
self
.
id
=
state
.
get
(
'id'
,
xml
.
get
(
'id'
))
self
.
id
=
state
.
get
(
'id'
,
xml
.
get
(
'id'
))
if
self
.
id
is
None
:
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'
,
''
)
self
.
value
=
state
.
get
(
'value'
,
''
)
...
@@ -157,6 +161,7 @@ class InputTypeBase(object):
...
@@ -157,6 +161,7 @@ class InputTypeBase(object):
self
.
msg
=
feedback
.
get
(
'message'
,
''
)
self
.
msg
=
feedback
.
get
(
'message'
,
''
)
self
.
hint
=
feedback
.
get
(
'hint'
,
''
)
self
.
hint
=
feedback
.
get
(
'hint'
,
''
)
self
.
hintmode
=
feedback
.
get
(
'hintmode'
,
None
)
self
.
hintmode
=
feedback
.
get
(
'hintmode'
,
None
)
self
.
input_state
=
state
.
get
(
'input_state'
,
{})
# put hint above msg if it should be displayed
# put hint above msg if it should be displayed
if
self
.
hintmode
==
'always'
:
if
self
.
hintmode
==
'always'
:
...
@@ -169,14 +174,15 @@ class InputTypeBase(object):
...
@@ -169,14 +174,15 @@ class InputTypeBase(object):
self
.
process_requirements
()
self
.
process_requirements
()
# Call subclass "constructor" -- means they don't have to worry about calling
# 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
()
self
.
setup
()
except
Exception
as
err
:
except
Exception
as
err
:
# Something went wrong: add xml to message, but keep the traceback
# 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
]
raise
Exception
,
msg
,
sys
.
exc_info
()[
2
]
@classmethod
@classmethod
def
get_attributes
(
cls
):
def
get_attributes
(
cls
):
"""
"""
...
@@ -186,7 +192,6 @@ class InputTypeBase(object):
...
@@ -186,7 +192,6 @@ class InputTypeBase(object):
"""
"""
return
[]
return
[]
def
process_requirements
(
self
):
def
process_requirements
(
self
):
"""
"""
Subclasses can declare lists of required and optional attributes. This
Subclasses can declare lists of required and optional attributes. This
...
@@ -196,7 +201,8 @@ class InputTypeBase(object):
...
@@ -196,7 +201,8 @@ class InputTypeBase(object):
Processes attributes, putting the results in the self.loaded_attributes dictionary. Also creates a set
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.
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
=
{}
loaded
=
{}
to_render
=
set
()
to_render
=
set
()
for
a
in
self
.
get_attributes
():
for
a
in
self
.
get_attributes
():
...
@@ -226,7 +232,7 @@ class InputTypeBase(object):
...
@@ -226,7 +232,7 @@ class InputTypeBase(object):
get: a dictionary containing the data that was sent with the ajax call
get: a dictionary containing the data that was sent with the ajax call
Output:
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
pass
...
@@ -247,8 +253,9 @@ class InputTypeBase(object):
...
@@ -247,8 +253,9 @@ class InputTypeBase(object):
'value'
:
self
.
value
,
'value'
:
self
.
value
,
'status'
:
self
.
status
,
'status'
:
self
.
status
,
'msg'
:
self
.
msg
,
'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
())
context
.
update
(
self
.
_extra_context
())
return
context
return
context
...
@@ -371,7 +378,6 @@ class ChoiceGroup(InputTypeBase):
...
@@ -371,7 +378,6 @@ class ChoiceGroup(InputTypeBase):
return
[
Attribute
(
"show_correctness"
,
"always"
),
return
[
Attribute
(
"show_correctness"
,
"always"
),
Attribute
(
"submitted_message"
,
"Answer received."
)]
Attribute
(
"submitted_message"
,
"Answer received."
)]
def
_extra_context
(
self
):
def
_extra_context
(
self
):
return
{
'input_type'
:
self
.
html_input_type
,
return
{
'input_type'
:
self
.
html_input_type
,
'choices'
:
self
.
choices
,
'choices'
:
self
.
choices
,
...
@@ -436,7 +442,6 @@ class JavascriptInput(InputTypeBase):
...
@@ -436,7 +442,6 @@ class JavascriptInput(InputTypeBase):
Attribute
(
'display_class'
,
None
),
Attribute
(
'display_class'
,
None
),
Attribute
(
'display_file'
,
None
),
]
Attribute
(
'display_file'
,
None
),
]
def
setup
(
self
):
def
setup
(
self
):
# Need to provide a value that JSON can parse if there is no
# Need to provide a value that JSON can parse if there is no
# student-supplied value yet.
# student-supplied value yet.
...
@@ -459,7 +464,6 @@ class TextLine(InputTypeBase):
...
@@ -459,7 +464,6 @@ class TextLine(InputTypeBase):
template
=
"textline.html"
template
=
"textline.html"
tags
=
[
'textline'
]
tags
=
[
'textline'
]
@classmethod
@classmethod
def
get_attributes
(
cls
):
def
get_attributes
(
cls
):
"""
"""
...
@@ -474,12 +478,12 @@ class TextLine(InputTypeBase):
...
@@ -474,12 +478,12 @@ class TextLine(InputTypeBase):
# Attributes below used in setup(), not rendered directly.
# Attributes below used in setup(), not rendered directly.
Attribute
(
'math'
,
None
,
render
=
False
),
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
(
'dojs'
,
None
,
render
=
False
),
Attribute
(
'preprocessorClassName'
,
None
,
render
=
False
),
Attribute
(
'preprocessorClassName'
,
None
,
render
=
False
),
Attribute
(
'preprocessorSrc'
,
None
,
render
=
False
),
Attribute
(
'preprocessorSrc'
,
None
,
render
=
False
),
]
]
def
setup
(
self
):
def
setup
(
self
):
self
.
do_math
=
bool
(
self
.
loaded_attributes
[
'math'
]
or
self
.
do_math
=
bool
(
self
.
loaded_attributes
[
'math'
]
or
...
@@ -490,12 +494,12 @@ class TextLine(InputTypeBase):
...
@@ -490,12 +494,12 @@ class TextLine(InputTypeBase):
self
.
preprocessor
=
None
self
.
preprocessor
=
None
if
self
.
do_math
:
if
self
.
do_math
:
# Preprocessor to insert between raw input and Mathjax
# Preprocessor to insert between raw input and Mathjax
self
.
preprocessor
=
{
'class_name'
:
self
.
loaded_attributes
[
'preprocessorClassName'
],
self
.
preprocessor
=
{
'script_src'
:
self
.
loaded_attributes
[
'preprocessorSrc'
]}
'class_name'
:
self
.
loaded_attributes
[
'preprocessorClassName'
],
'script_src'
:
self
.
loaded_attributes
[
'preprocessorSrc'
]}
if
None
in
self
.
preprocessor
.
values
():
if
None
in
self
.
preprocessor
.
values
():
self
.
preprocessor
=
None
self
.
preprocessor
=
None
def
_extra_context
(
self
):
def
_extra_context
(
self
):
return
{
'do_math'
:
self
.
do_math
,
return
{
'do_math'
:
self
.
do_math
,
'preprocessor'
:
self
.
preprocessor
,
}
'preprocessor'
:
self
.
preprocessor
,
}
...
@@ -539,7 +543,8 @@ class FileSubmission(InputTypeBase):
...
@@ -539,7 +543,8 @@ class FileSubmission(InputTypeBase):
"""
"""
# Check if problem has been queued
# Check if problem has been queued
self
.
queue_len
=
0
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'
:
if
self
.
status
==
'incomplete'
:
self
.
status
=
'queued'
self
.
status
=
'queued'
self
.
queue_len
=
self
.
msg
self
.
queue_len
=
self
.
msg
...
@@ -547,7 +552,6 @@ class FileSubmission(InputTypeBase):
...
@@ -547,7 +552,6 @@ class FileSubmission(InputTypeBase):
def
_extra_context
(
self
):
def
_extra_context
(
self
):
return
{
'queue_len'
:
self
.
queue_len
,
}
return
{
'queue_len'
:
self
.
queue_len
,
}
return
context
registry
.
register
(
FileSubmission
)
registry
.
register
(
FileSubmission
)
...
@@ -562,8 +566,9 @@ class CodeInput(InputTypeBase):
...
@@ -562,8 +566,9 @@ class CodeInput(InputTypeBase):
template
=
"codeinput.html"
template
=
"codeinput.html"
tags
=
[
'codeinput'
,
tags
=
[
'codeinput'
,
'textbox'
,
# Another (older) name--at some point we may want to make it use a
'textbox'
,
# non-codemirror editor.
# Another (older) name--at some point we may want to make it use a
# non-codemirror editor.
]
]
# pulled out for testing
# pulled out for testing
...
@@ -586,22 +591,29 @@ class CodeInput(InputTypeBase):
...
@@ -586,22 +591,29 @@ class CodeInput(InputTypeBase):
Attribute
(
'tabsize'
,
4
,
transform
=
int
),
Attribute
(
'tabsize'
,
4
,
transform
=
int
),
]
]
def
setup
(
self
):
def
setup
_code_response_rendering
(
self
):
"""
"""
Implement special logic: handle queueing state, and default input.
Implement special logic: handle queueing state, and default input.
"""
"""
# if no student input yet, then use the default input given by the problem
# if no student input yet, then use the default input given by the
if
not
self
.
value
:
# problem
self
.
value
=
self
.
xml
.
text
if
not
self
.
value
and
self
.
xml
.
text
:
self
.
value
=
self
.
xml
.
text
.
strip
()
# Check if problem has been queued
# Check if problem has been queued
self
.
queue_len
=
0
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'
:
if
self
.
status
==
'incomplete'
:
self
.
status
=
'queued'
self
.
status
=
'queued'
self
.
queue_len
=
self
.
msg
self
.
queue_len
=
self
.
msg
self
.
msg
=
self
.
submitted_msg
self
.
msg
=
self
.
submitted_msg
def
setup
(
self
):
''' setup this input type '''
self
.
setup_code_response_rendering
()
def
_extra_context
(
self
):
def
_extra_context
(
self
):
"""Defined queue_len, add it """
"""Defined queue_len, add it """
return
{
'queue_len'
:
self
.
queue_len
,
}
return
{
'queue_len'
:
self
.
queue_len
,
}
...
@@ -610,8 +622,164 @@ registry.register(CodeInput)
...
@@ -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
):
class
Schematic
(
InputTypeBase
):
"""
"""
InputType for the schematic editor
"""
"""
template
=
"schematicinput.html"
template
=
"schematicinput.html"
...
@@ -630,7 +798,6 @@ class Schematic(InputTypeBase):
...
@@ -630,7 +798,6 @@ class Schematic(InputTypeBase):
Attribute
(
'initial_value'
,
None
),
Attribute
(
'initial_value'
,
None
),
Attribute
(
'submit_analyses'
,
None
),
]
Attribute
(
'submit_analyses'
,
None
),
]
return
context
registry
.
register
(
Schematic
)
registry
.
register
(
Schematic
)
...
@@ -660,12 +827,12 @@ class ImageInput(InputTypeBase):
...
@@ -660,12 +827,12 @@ class ImageInput(InputTypeBase):
Attribute
(
'height'
),
Attribute
(
'height'
),
Attribute
(
'width'
),
]
Attribute
(
'width'
),
]
def
setup
(
self
):
def
setup
(
self
):
"""
"""
if value is of the form [x,y] then parse it and send along coordinates of previous answer
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
:
if
m
:
# Note: we subtract 15 to compensate for the size of the dot on the screen.
# Note: we subtract 15 to compensate for the size of the dot on the screen.
# (is a 30x30 image--lms/static/green-pointer.png).
# (is a 30x30 image--lms/static/green-pointer.png).
...
@@ -673,7 +840,6 @@ class ImageInput(InputTypeBase):
...
@@ -673,7 +840,6 @@ class ImageInput(InputTypeBase):
else
:
else
:
(
self
.
gx
,
self
.
gy
)
=
(
0
,
0
)
(
self
.
gx
,
self
.
gy
)
=
(
0
,
0
)
def
_extra_context
(
self
):
def
_extra_context
(
self
):
return
{
'gx'
:
self
.
gx
,
return
{
'gx'
:
self
.
gx
,
...
@@ -730,7 +896,7 @@ class VseprInput(InputTypeBase):
...
@@ -730,7 +896,7 @@ class VseprInput(InputTypeBase):
registry
.
register
(
VseprInput
)
registry
.
register
(
VseprInput
)
#-------------------------------------------------------------------------
-------
#-------------------------------------------------------------------------
class
ChemicalEquationInput
(
InputTypeBase
):
class
ChemicalEquationInput
(
InputTypeBase
):
...
@@ -794,7 +960,8 @@ class ChemicalEquationInput(InputTypeBase):
...
@@ -794,7 +960,8 @@ class ChemicalEquationInput(InputTypeBase):
result
[
'error'
]
=
"Couldn't parse formula: {0}"
.
format
(
p
)
result
[
'error'
]
=
"Couldn't parse formula: {0}"
.
format
(
p
)
except
Exception
:
except
Exception
:
# this is unexpected, so log
# 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"
result
[
'error'
]
=
"Error while rendering preview"
return
result
return
result
...
@@ -843,16 +1010,16 @@ class DragAndDropInput(InputTypeBase):
...
@@ -843,16 +1010,16 @@ class DragAndDropInput(InputTypeBase):
'can_reuse'
:
""
}
'can_reuse'
:
""
}
tag_attrs
[
'target'
]
=
{
'id'
:
Attribute
.
_sentinel
,
tag_attrs
[
'target'
]
=
{
'id'
:
Attribute
.
_sentinel
,
'x'
:
Attribute
.
_sentinel
,
'x'
:
Attribute
.
_sentinel
,
'y'
:
Attribute
.
_sentinel
,
'y'
:
Attribute
.
_sentinel
,
'w'
:
Attribute
.
_sentinel
,
'w'
:
Attribute
.
_sentinel
,
'h'
:
Attribute
.
_sentinel
}
'h'
:
Attribute
.
_sentinel
}
dic
=
dict
()
dic
=
dict
()
for
attr_name
in
tag_attrs
[
tag_type
]
.
keys
():
for
attr_name
in
tag_attrs
[
tag_type
]
.
keys
():
dic
[
attr_name
]
=
Attribute
(
attr_name
,
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
:
if
tag_type
==
'draggable'
and
not
self
.
no_labels
:
dic
[
'label'
]
=
dic
[
'label'
]
or
dic
[
'id'
]
dic
[
'label'
]
=
dic
[
'label'
]
or
dic
[
'id'
]
...
@@ -865,7 +1032,7 @@ class DragAndDropInput(InputTypeBase):
...
@@ -865,7 +1032,7 @@ class DragAndDropInput(InputTypeBase):
# add labels to images?:
# add labels to images?:
self
.
no_labels
=
Attribute
(
'no_labels'
,
self
.
no_labels
=
Attribute
(
'no_labels'
,
default
=
"False"
)
.
parse_from_xml
(
self
.
xml
)
default
=
"False"
)
.
parse_from_xml
(
self
.
xml
)
to_js
=
dict
()
to_js
=
dict
()
...
@@ -874,16 +1041,16 @@ class DragAndDropInput(InputTypeBase):
...
@@ -874,16 +1041,16 @@ class DragAndDropInput(InputTypeBase):
# outline places on image where to drag adn drop
# outline places on image where to drag adn drop
to_js
[
'target_outline'
]
=
Attribute
(
'target_outline'
,
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?
# one draggable per target?
to_js
[
'one_per_target'
]
=
Attribute
(
'one_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
# list of draggables
to_js
[
'draggables'
]
=
[
parse
(
draggable
,
'draggable'
)
for
draggable
in
to_js
[
'draggables'
]
=
[
parse
(
draggable
,
'draggable'
)
for
draggable
in
self
.
xml
.
iterchildren
(
'draggable'
)]
self
.
xml
.
iterchildren
(
'draggable'
)]
# list of targets
# list of targets
to_js
[
'targets'
]
=
[
parse
(
target
,
'target'
)
for
target
in
to_js
[
'targets'
]
=
[
parse
(
target
,
'target'
)
for
target
in
self
.
xml
.
iterchildren
(
'target'
)]
self
.
xml
.
iterchildren
(
'target'
)]
# custom background color for labels:
# custom background color for labels:
label_bg_color
=
Attribute
(
'label_bg_color'
,
label_bg_color
=
Attribute
(
'label_bg_color'
,
...
@@ -896,7 +1063,7 @@ class DragAndDropInput(InputTypeBase):
...
@@ -896,7 +1063,7 @@ class DragAndDropInput(InputTypeBase):
registry
.
register
(
DragAndDropInput
)
registry
.
register
(
DragAndDropInput
)
#-------------------------------------------------------------------------
-------------------------------------------
#-------------------------------------------------------------------------
class
EditAMoleculeInput
(
InputTypeBase
):
class
EditAMoleculeInput
(
InputTypeBase
):
...
@@ -934,6 +1101,7 @@ registry.register(EditAMoleculeInput)
...
@@ -934,6 +1101,7 @@ registry.register(EditAMoleculeInput)
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
class
DesignProtein2dInput
(
InputTypeBase
):
class
DesignProtein2dInput
(
InputTypeBase
):
"""
"""
An input type for design of a protein in 2D. Integrates with the Protex java applet.
An input type for design of a protein in 2D. Integrates with the Protex java applet.
...
@@ -969,6 +1137,7 @@ registry.register(DesignProtein2dInput)
...
@@ -969,6 +1137,7 @@ registry.register(DesignProtein2dInput)
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
class
EditAGeneInput
(
InputTypeBase
):
class
EditAGeneInput
(
InputTypeBase
):
"""
"""
An input type for editing a gene. Integrates with the genex java applet.
An input type for editing a gene. Integrates with the genex java applet.
...
@@ -1005,6 +1174,7 @@ registry.register(EditAGeneInput)
...
@@ -1005,6 +1174,7 @@ registry.register(EditAGeneInput)
#---------------------------------------------------------------------
#---------------------------------------------------------------------
class
AnnotationInput
(
InputTypeBase
):
class
AnnotationInput
(
InputTypeBase
):
"""
"""
Input type for annotations: students can enter some notes or other text
Input type for annotations: students can enter some notes or other text
...
@@ -1037,13 +1207,14 @@ class AnnotationInput(InputTypeBase):
...
@@ -1037,13 +1207,14 @@ class AnnotationInput(InputTypeBase):
def
setup
(
self
):
def
setup
(
self
):
xml
=
self
.
xml
xml
=
self
.
xml
self
.
debug
=
False
# set to True to display extra debug info with input
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
.
return_to_annotation
=
True
# return only works in conjunction with annotatable xmodule
self
.
title
=
xml
.
findtext
(
'./title'
,
'Annotation Exercise'
)
self
.
title
=
xml
.
findtext
(
'./title'
,
'Annotation Exercise'
)
self
.
text
=
xml
.
findtext
(
'./text'
)
self
.
text
=
xml
.
findtext
(
'./text'
)
self
.
comment
=
xml
.
findtext
(
'./comment'
)
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
.
tag_prompt
=
xml
.
findtext
(
'./tag_prompt'
,
'Select one tag:'
)
self
.
options
=
self
.
_find_options
()
self
.
options
=
self
.
_find_options
()
...
@@ -1061,7 +1232,7 @@ class AnnotationInput(InputTypeBase):
...
@@ -1061,7 +1232,7 @@ class AnnotationInput(InputTypeBase):
'id'
:
index
,
'id'
:
index
,
'description'
:
option
.
text
,
'description'
:
option
.
text
,
'choice'
:
option
.
get
(
'choice'
)
'choice'
:
option
.
get
(
'choice'
)
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
def
_validate_options
(
self
):
def
_validate_options
(
self
):
''' Raises a ValueError if the choice attribute is missing or invalid. '''
''' Raises a ValueError if the choice attribute is missing or invalid. '''
...
@@ -1071,7 +1242,8 @@ class AnnotationInput(InputTypeBase):
...
@@ -1071,7 +1242,8 @@ class AnnotationInput(InputTypeBase):
if
choice
is
None
:
if
choice
is
None
:
raise
ValueError
(
'Missing required choice attribute.'
)
raise
ValueError
(
'Missing required choice attribute.'
)
elif
choice
not
in
valid_choices
:
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
):
def
_unpack
(
self
,
json_value
):
''' Unpacks the json input state into a dict. '''
''' Unpacks the json input state into a dict. '''
...
@@ -1089,20 +1261,20 @@ class AnnotationInput(InputTypeBase):
...
@@ -1089,20 +1261,20 @@ class AnnotationInput(InputTypeBase):
return
{
return
{
'options_value'
:
options_value
,
'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
,
'comment_value'
:
comment_value
,
}
}
def
_extra_context
(
self
):
def
_extra_context
(
self
):
extra_context
=
{
extra_context
=
{
'title'
:
self
.
title
,
'title'
:
self
.
title
,
'text'
:
self
.
text
,
'text'
:
self
.
text
,
'comment'
:
self
.
comment
,
'comment'
:
self
.
comment
,
'comment_prompt'
:
self
.
comment_prompt
,
'comment_prompt'
:
self
.
comment_prompt
,
'tag_prompt'
:
self
.
tag_prompt
,
'tag_prompt'
:
self
.
tag_prompt
,
'options'
:
self
.
options
,
'options'
:
self
.
options
,
'return_to_annotation'
:
self
.
return_to_annotation
,
'return_to_annotation'
:
self
.
return_to_annotation
,
'debug'
:
self
.
debug
'debug'
:
self
.
debug
}
}
extra_context
.
update
(
self
.
_unpack
(
self
.
value
))
extra_context
.
update
(
self
.
_unpack
(
self
.
value
))
...
@@ -1110,4 +1282,3 @@ class AnnotationInput(InputTypeBase):
...
@@ -1110,4 +1282,3 @@ class AnnotationInput(InputTypeBase):
return
extra_context
return
extra_context
registry
.
register
(
AnnotationInput
)
registry
.
register
(
AnnotationInput
)
common/lib/capa/capa/responsetypes.py
View file @
f0e1b477
...
@@ -128,21 +128,25 @@ class LoncapaResponse(object):
...
@@ -128,21 +128,25 @@ class LoncapaResponse(object):
for
abox
in
inputfields
:
for
abox
in
inputfields
:
if
abox
.
tag
not
in
self
.
allowed_inputfields
:
if
abox
.
tag
not
in
self
.
allowed_inputfields
:
msg
=
"
%
s: cannot have input field
%
s"
%
(
unicode
(
self
),
abox
.
tag
)
msg
=
"
%
s: cannot have input field
%
s"
%
(
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
unicode
(
self
),
abox
.
tag
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
raise
LoncapaProblemError
(
msg
)
if
self
.
max_inputfields
and
len
(
inputfields
)
>
self
.
max_inputfields
:
if
self
.
max_inputfields
and
len
(
inputfields
)
>
self
.
max_inputfields
:
msg
=
"
%
s: cannot have more than
%
s input fields"
%
(
msg
=
"
%
s: cannot have more than
%
s input fields"
%
(
unicode
(
self
),
self
.
max_inputfields
)
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
)
raise
LoncapaProblemError
(
msg
)
for
prop
in
self
.
required_attributes
:
for
prop
in
self
.
required_attributes
:
if
not
xml
.
get
(
prop
):
if
not
xml
.
get
(
prop
):
msg
=
"Error in problem specification:
%
s missing required attribute
%
s"
%
(
msg
=
"Error in problem specification:
%
s missing required attribute
%
s"
%
(
unicode
(
self
),
prop
)
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
)
raise
LoncapaProblemError
(
msg
)
# ordered list of answer_id values for this response
# ordered list of answer_id values for this response
...
@@ -163,7 +167,8 @@ class LoncapaResponse(object):
...
@@ -163,7 +167,8 @@ class LoncapaResponse(object):
for
entry
in
self
.
inputfields
:
for
entry
in
self
.
inputfields
:
answer
=
entry
.
get
(
'correct_answer'
)
answer
=
entry
.
get
(
'correct_answer'
)
if
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'
):
if
hasattr
(
self
,
'setup_response'
):
self
.
setup_response
()
self
.
setup_response
()
...
@@ -211,7 +216,8 @@ class LoncapaResponse(object):
...
@@ -211,7 +216,8 @@ class LoncapaResponse(object):
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
'''
'''
new_cmap
=
self
.
get_score
(
student_answers
)
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)
# log.debug('new_cmap = %s' % new_cmap)
return
new_cmap
return
new_cmap
...
@@ -241,14 +247,17 @@ class LoncapaResponse(object):
...
@@ -241,14 +247,17 @@ class LoncapaResponse(object):
# callback procedure to a social hint generation system.
# callback procedure to a social hint generation system.
if
not
hintfn
in
self
.
context
:
if
not
hintfn
in
self
.
context
:
msg
=
'missing specified hint function
%
s in script context'
%
hintfn
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
)
raise
LoncapaProblemError
(
msg
)
try
:
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
:
except
Exception
as
err
:
msg
=
'Error
%
s in evaluating hint function
%
s'
%
(
err
,
hintfn
)
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
)
raise
ResponseError
(
msg
)
return
return
...
@@ -270,17 +279,19 @@ class LoncapaResponse(object):
...
@@ -270,17 +279,19 @@ class LoncapaResponse(object):
if
(
self
.
hint_tag
is
not
None
if
(
self
.
hint_tag
is
not
None
and
hintgroup
.
find
(
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
)
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)
# can be 'on_request' or 'always' (default)
hintmode
=
hintgroup
.
get
(
'mode'
,
'always'
)
hintmode
=
hintgroup
.
get
(
'mode'
,
'always'
)
for
hintpart
in
hintgroup
.
findall
(
'hintpart'
):
for
hintpart
in
hintgroup
.
findall
(
'hintpart'
):
if
hintpart
.
get
(
'on'
)
in
hints_to_show
:
if
hintpart
.
get
(
'on'
)
in
hints_to_show
:
hint_text
=
hintpart
.
find
(
'text'
)
.
text
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
]
aid
=
self
.
answer_ids
[
-
1
]
new_cmap
.
set_hint_and_mode
(
aid
,
hint_text
,
hintmode
)
new_cmap
.
set_hint_and_mode
(
aid
,
hint_text
,
hintmode
)
log
.
debug
(
'after hint: new_cmap =
%
s'
%
new_cmap
)
log
.
debug
(
'after hint: new_cmap =
%
s'
%
new_cmap
)
...
@@ -340,7 +351,6 @@ class LoncapaResponse(object):
...
@@ -340,7 +351,6 @@ class LoncapaResponse(object):
response_msg_div
=
etree
.
Element
(
'div'
)
response_msg_div
=
etree
.
Element
(
'div'
)
response_msg_div
.
text
=
str
(
response_msg
)
response_msg_div
.
text
=
str
(
response_msg
)
# Set the css class of the message <div>
# Set the css class of the message <div>
response_msg_div
.
set
(
"class"
,
"response_message"
)
response_msg_div
.
set
(
"class"
,
"response_message"
)
...
@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse):
# until we decide on exactly how to solve this issue. For now, files are
# until we decide on exactly how to solve this issue. For now, files are
# manually being compiled to DATA_DIR/js/compiled.
# manually being compiled to DATA_DIR/js/compiled.
#latestTimestamp = 0
#
latestTimestamp = 0
#basepath = self.system.filestore.root_path + '/js/'
#
basepath = self.system.filestore.root_path + '/js/'
#for filename in (self.display_dependencies + [self.display]):
#
for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
# filepath = basepath + filename
# timestamp = os.stat(filepath).st_mtime
# timestamp = os.stat(filepath).st_mtime
# if timestamp > latestTimestamp:
# if timestamp > latestTimestamp:
# latestTimestamp = timestamp
# latestTimestamp = timestamp
#
#
#h = hashlib.md5()
#
h = hashlib.md5()
#h.update(self.answer_id + str(self.display_dependencies))
#
h.update(self.answer_id + str(self.display_dependencies))
#compiled_filename = 'compiled/' + h.hexdigest() + '.js'
#
compiled_filename = 'compiled/' + h.hexdigest() + '.js'
#compiled_filepath = basepath + compiled_filename
#
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')
# outfile = open(compiled_filepath, 'w')
# for filename in (self.display_dependencies + [self.display]):
# for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
# filepath = basepath + filename
...
@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse):
id
=
self
.
xml
.
get
(
'id'
))[
0
]
id
=
self
.
xml
.
get
(
'id'
))[
0
]
self
.
display_xml
=
self
.
xml
.
xpath
(
'//*[@id=$id]//display'
,
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
.
generator_xml
)
self
.
xml
.
remove
(
self
.
grader_xml
)
self
.
xml
.
remove
(
self
.
grader_xml
)
...
@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse):
self
.
display
=
self
.
display_xml
.
get
(
"src"
)
self
.
display
=
self
.
display_xml
.
get
(
"src"
)
if
self
.
generator_xml
.
get
(
"dependencies"
):
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
:
else
:
self
.
generator_dependencies
=
[]
self
.
generator_dependencies
=
[]
if
self
.
grader_xml
.
get
(
"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
:
else
:
self
.
grader_dependencies
=
[]
self
.
grader_dependencies
=
[]
if
self
.
display_xml
.
get
(
"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
:
else
:
self
.
display_dependencies
=
[]
self
.
display_dependencies
=
[]
...
@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse):
return
subprocess
.
check_output
(
subprocess_args
,
env
=
self
.
get_node_env
())
return
subprocess
.
check_output
(
subprocess_args
,
env
=
self
.
get_node_env
())
def
generate_problem_state
(
self
):
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
,
output
=
self
.
call_node
([
generator_file
,
self
.
generator
,
self
.
generator
,
json
.
dumps
(
self
.
generator_dependencies
),
json
.
dumps
(
self
.
generator_dependencies
),
...
@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse):
params
=
{}
params
=
{}
for
param
in
self
.
xml
.
xpath
(
'//*[@id=$id]//responseparam'
,
for
param
in
self
.
xml
.
xpath
(
'//*[@id=$id]//responseparam'
,
id
=
self
.
xml
.
get
(
'id'
)):
id
=
self
.
xml
.
get
(
'id'
)):
raw_param
=
param
.
get
(
"value"
)
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
return
params
def
prepare_inputfield
(
self
):
def
prepare_inputfield
(
self
):
for
inputfield
in
self
.
xml
.
xpath
(
'//*[@id=$id]//javascriptinput'
,
for
inputfield
in
self
.
xml
.
xpath
(
'//*[@id=$id]//javascriptinput'
,
id
=
self
.
xml
.
get
(
'id'
)):
id
=
self
.
xml
.
get
(
'id'
)):
escapedict
=
{
'"'
:
'"'
}
escapedict
=
{
'"'
:
'"'
}
...
@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse):
escapedict
)
escapedict
)
inputfield
.
set
(
"problem_state"
,
encoded_problem_state
)
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
)
inputfield
.
set
(
"display_class"
,
self
.
display_class
)
def
get_score
(
self
,
student_answers
):
def
get_score
(
self
,
student_answers
):
...
@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse):
if
submission
is
None
or
submission
==
''
:
if
submission
is
None
or
submission
==
''
:
submission
=
json
.
dumps
(
None
)
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
,
outputs
=
self
.
call_node
([
grader_file
,
self
.
grader
,
self
.
grader
,
json
.
dumps
(
self
.
grader_dependencies
),
json
.
dumps
(
self
.
grader_dependencies
),
...
@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse):
json
.
dumps
(
self
.
params
)])
.
split
(
'
\n
'
)
json
.
dumps
(
self
.
params
)])
.
split
(
'
\n
'
)
all_correct
=
json
.
loads
(
outputs
[
0
]
.
strip
())
all_correct
=
json
.
loads
(
outputs
[
0
]
.
strip
())
evaluation
=
outputs
[
1
]
.
strip
()
evaluation
=
outputs
[
1
]
.
strip
()
solution
=
outputs
[
2
]
.
strip
()
solution
=
outputs
[
2
]
.
strip
()
return
(
all_correct
,
evaluation
,
solution
)
return
(
all_correct
,
evaluation
,
solution
)
def
get_answers
(
self
):
def
get_answers
(
self
):
...
@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse):
...
@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse):
return
{
self
.
answer_id
:
self
.
solution
}
return
{
self
.
answer_id
:
self
.
solution
}
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
class
ChoiceResponse
(
LoncapaResponse
):
class
ChoiceResponse
(
LoncapaResponse
):
"""
"""
This response type is used when the student chooses from a discrete set of
This response type is used when the student chooses from a discrete set of
...
@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse):
...
@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse):
self
.
assign_choice_names
()
self
.
assign_choice_names
()
correct_xml
=
self
.
xml
.
xpath
(
'//*[@id=$id]//choice[@correct="true"]'
,
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
):
def
assign_choice_names
(
self
):
'''
'''
...
@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse):
...
@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse):
allowed_inputfields
=
[
'choicegroup'
]
allowed_inputfields
=
[
'choicegroup'
]
def
setup_response
(
self
):
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
()
self
.
mc_setup_response
()
# define correct choices (after calling secondary setup)
# define correct choices (after calling secondary setup)
...
@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
...
@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
# unicode(self), student_answers, self.correct_choices))
# unicode(self), student_answers, self.correct_choices))
if
(
self
.
answer_id
in
student_answers
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'
)
return
CorrectMap
(
self
.
answer_id
,
'correct'
)
else
:
else
:
return
CorrectMap
(
self
.
answer_id
,
'incorrect'
)
return
CorrectMap
(
self
.
answer_id
,
'incorrect'
)
...
@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse):
...
@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse):
return
cmap
return
cmap
def
get_answers
(
self
):
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))
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
return
amap
return
amap
...
@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse):
...
@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse):
context
=
self
.
context
context
=
self
.
context
self
.
correct_answer
=
contextualize_text
(
xml
.
get
(
'answer'
),
context
)
self
.
correct_answer
=
contextualize_text
(
xml
.
get
(
'answer'
),
context
)
try
:
try
:
self
.
tolerance_xml
=
xml
.
xpath
(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
self
.
tolerance_xml
=
xml
.
xpath
(
id
=
xml
.
get
(
'id'
))[
0
]
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
id
=
xml
.
get
(
'id'
))[
0
]
self
.
tolerance
=
contextualize_text
(
self
.
tolerance_xml
,
context
)
self
.
tolerance
=
contextualize_text
(
self
.
tolerance_xml
,
context
)
except
Exception
:
except
Exception
:
self
.
tolerance
=
'0'
self
.
tolerance
=
'0'
...
@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse):
...
@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse):
try
:
try
:
correct_ans
=
complex
(
self
.
correct_answer
)
correct_ans
=
complex
(
self
.
correct_answer
)
except
ValueError
:
except
ValueError
:
log
.
debug
(
"Content error--answer '{0}' is not a valid complex number"
.
format
(
self
.
correct_answer
))
log
.
debug
(
"Content error--answer '{0}' is not a valid complex number"
.
format
(
raise
StudentInputError
(
"There was a problem with the staff answer to this problem"
)
self
.
correct_answer
))
raise
StudentInputError
(
"There was a problem with the staff answer to this problem"
)
try
:
try
:
correct
=
compare_with_tolerance
(
evaluator
(
dict
(),
dict
(),
student_answer
),
correct
=
compare_with_tolerance
(
correct_ans
,
self
.
tolerance
)
evaluator
(
dict
(),
dict
(),
student_answer
),
correct_ans
,
self
.
tolerance
)
# We should catch this explicitly.
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
# But we'd need to confirm
except
:
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
import
sys
type
,
value
,
traceback
=
sys
.
exc_info
()
type
,
value
,
traceback
=
sys
.
exc_info
()
...
@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse):
...
@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse):
max_inputfields
=
1
max_inputfields
=
1
def
setup_response
(
self
):
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
):
def
get_score
(
self
,
student_answers
):
'''Grade a string response '''
'''Grade a string response '''
...
@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse):
...
@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse):
return
CorrectMap
(
self
.
answer_id
,
'correct'
if
correct
else
'incorrect'
)
return
CorrectMap
(
self
.
answer_id
,
'correct'
if
correct
else
'incorrect'
)
def
check_string
(
self
,
expected
,
given
):
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
return
given
==
expected
def
check_hint_condition
(
self
,
hxml_set
,
student_answers
):
def
check_hint_condition
(
self
,
hxml_set
,
student_answers
):
...
@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse):
...
@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse):
hints_to_show
=
[]
hints_to_show
=
[]
for
hxml
in
hxml_set
:
for
hxml
in
hxml_set
:
name
=
hxml
.
get
(
'name'
)
name
=
hxml
.
get
(
'name'
)
correct_answer
=
contextualize_text
(
hxml
.
get
(
'answer'
),
self
.
context
)
.
strip
()
correct_answer
=
contextualize_text
(
if
self
.
check_string
(
correct_answer
,
given
):
hints_to_show
.
append
(
name
)
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
return
hints_to_show
...
@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse):
...
@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse):
correct[0] ='incorrect'
correct[0] ='incorrect'
</answer>
</answer>
</customresponse>"""
},
</customresponse>"""
},
{
'snippet'
:
"""<script type="loncapa/python"><![CDATA[
{
'snippet'
:
"""<script type="loncapa/python"><![CDATA[
def sympy_check2():
def sympy_check2():
messages[0] = '
%
s:
%
s'
%
(submission[0],fromjs[0].replace('<','<'))
messages[0] = '
%
s:
%
s'
%
(submission[0],fromjs[0].replace('<','<'))
...
@@ -907,15 +932,16 @@ def sympy_check2():
...
@@ -907,15 +932,16 @@ def sympy_check2():
response_tag
=
'customresponse'
response_tag
=
'customresponse'
allowed_inputfields
=
[
'textline'
,
'textbox'
,
'crystallography'
,
allowed_inputfields
=
[
'textline'
,
'textbox'
,
'crystallography'
,
'chemicalequationinput'
,
'vsepr_input'
,
'chemicalequationinput'
,
'vsepr_input'
,
'drag_and_drop_input'
,
'editamoleculeinput'
,
'drag_and_drop_input'
,
'editamoleculeinput'
,
'designprotein2dinput'
,
'editageneinput'
,
'designprotein2dinput'
,
'editageneinput'
,
'annotationinput'
]
'annotationinput'
]
def
setup_response
(
self
):
def
setup_response
(
self
):
xml
=
self
.
xml
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
.
expect
=
xml
.
get
(
'expect'
)
or
xml
.
get
(
'answer'
)
self
.
myid
=
xml
.
get
(
'id'
)
self
.
myid
=
xml
.
get
(
'id'
)
...
@@ -939,7 +965,8 @@ def sympy_check2():
...
@@ -939,7 +965,8 @@ def sympy_check2():
if
cfn
in
self
.
context
:
if
cfn
in
self
.
context
:
self
.
code
=
self
.
context
[
cfn
]
self
.
code
=
self
.
context
[
cfn
]
else
:
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'
,
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
raise
LoncapaProblemError
(
msg
)
...
@@ -952,7 +979,8 @@ def sympy_check2():
...
@@ -952,7 +979,8 @@ def sympy_check2():
else
:
else
:
answer_src
=
answer
.
get
(
'src'
)
answer_src
=
answer
.
get
(
'src'
)
if
answer_src
is
not
None
:
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
:
else
:
self
.
code
=
answer
.
text
self
.
code
=
answer
.
text
...
@@ -1032,7 +1060,7 @@ def sympy_check2():
...
@@ -1032,7 +1060,7 @@ def sympy_check2():
# any options to be passed to the cfn
# any options to be passed to the cfn
'options'
:
self
.
xml
.
get
(
'options'
),
'options'
:
self
.
xml
.
get
(
'options'
),
'testdat'
:
'hello world'
,
'testdat'
:
'hello world'
,
})
})
# pass self.system.debug to cfn
# pass self.system.debug to cfn
self
.
context
[
'debug'
]
=
self
.
system
.
DEBUG
self
.
context
[
'debug'
]
=
self
.
system
.
DEBUG
...
@@ -1049,7 +1077,8 @@ def sympy_check2():
...
@@ -1049,7 +1077,8 @@ def sympy_check2():
print
"context = "
,
self
.
context
print
"context = "
,
self
.
context
print
traceback
.
format_exc
()
print
traceback
.
format_exc
()
# Notify student
# 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
:
else
:
# self.code is not a string; assume its a function
# self.code is not a string; assume its a function
...
@@ -1058,18 +1087,22 @@ def sympy_check2():
...
@@ -1058,18 +1087,22 @@ def sympy_check2():
ret
=
None
ret
=
None
log
.
debug
(
" submission =
%
s"
%
submission
)
log
.
debug
(
" submission =
%
s"
%
submission
)
try
:
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
# handle variable number of arguments in check function, for backwards compatibility
# with various Tutor2 check functions
# 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
)
argspec
=
inspect
.
getargspec
(
fn
)
nargs
=
len
(
argspec
.
args
)
-
len
(
argspec
.
defaults
or
[])
nargs
=
len
(
argspec
.
args
)
-
len
(
argspec
.
defaults
or
[])
kwargs
=
{}
kwargs
=
{}
for
argname
in
argspec
.
args
[
nargs
:]:
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
(
'[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
)
ret
=
fn
(
*
args
[:
nargs
],
**
kwargs
)
except
Exception
as
err
:
except
Exception
as
err
:
...
@@ -1077,7 +1110,8 @@ def sympy_check2():
...
@@ -1077,7 +1110,8 @@ def sympy_check2():
# print "context = ",self.context
# print "context = ",self.context
log
.
error
(
traceback
.
format_exc
())
log
.
error
(
traceback
.
format_exc
())
raise
Exception
(
"oops in customresponse (cfn) error
%
s"
%
err
)
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
:
if
type
(
ret
)
==
dict
:
...
@@ -1086,7 +1120,8 @@ def sympy_check2():
...
@@ -1086,7 +1120,8 @@ def sympy_check2():
# If there are multiple inputs, they all get marked
# If there are multiple inputs, they all get marked
# to the same correct/incorrect value
# to the same correct/incorrect value
if
'ok'
in
ret
:
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
=
ret
.
get
(
'msg'
,
None
)
msg
=
self
.
clean_message_html
(
msg
)
msg
=
self
.
clean_message_html
(
msg
)
...
@@ -1097,7 +1132,6 @@ def sympy_check2():
...
@@ -1097,7 +1132,6 @@ def sympy_check2():
else
:
else
:
messages
[
0
]
=
msg
messages
[
0
]
=
msg
# Another kind of dictionary the check function can return has
# Another kind of dictionary the check function can return has
# the form:
# the form:
# {'overall_message': STRING,
# {'overall_message': STRING,
...
@@ -1113,21 +1147,25 @@ def sympy_check2():
...
@@ -1113,21 +1147,25 @@ def sympy_check2():
correct
=
[]
correct
=
[]
messages
=
[]
messages
=
[]
for
input_dict
in
input_list
:
for
input_dict
in
input_list
:
correct
.
append
(
'correct'
if
input_dict
[
'ok'
]
else
'incorrect'
)
correct
.
append
(
'correct'
msg
=
self
.
clean_message_html
(
input_dict
[
'msg'
])
if
'msg'
in
input_dict
else
None
if
input_dict
[
'ok'
]
else
'incorrect'
)
msg
=
(
self
.
clean_message_html
(
input_dict
[
'msg'
])
if
'msg'
in
input_dict
else
None
)
messages
.
append
(
msg
)
messages
.
append
(
msg
)
# Otherwise, we do not recognize the dictionary
# Otherwise, we do not recognize the dictionary
# Raise an exception
# Raise an exception
else
:
else
:
log
.
error
(
traceback
.
format_exc
())
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,
# The check function can return a boolean value,
# indicating whether all inputs should be marked
# indicating whether all inputs should be marked
# correct or incorrect
# correct or incorrect
else
:
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)
# build map giving "correct"ness of the answer(s)
correct_map
=
CorrectMap
()
correct_map
=
CorrectMap
()
...
@@ -1136,7 +1174,8 @@ def sympy_check2():
...
@@ -1136,7 +1174,8 @@ def sympy_check2():
correct_map
.
set_overall_message
(
overall_message
)
correct_map
.
set_overall_message
(
overall_message
)
for
k
in
range
(
len
(
idset
)):
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
],
correct_map
.
set
(
idset
[
k
],
correct
[
k
],
msg
=
messages
[
k
],
npoints
=
npoints
)
npoints
=
npoints
)
return
correct_map
return
correct_map
...
@@ -1232,8 +1271,9 @@ class CodeResponse(LoncapaResponse):
...
@@ -1232,8 +1271,9 @@ class CodeResponse(LoncapaResponse):
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
system.xqueue = { 'interface': XqueueInterface object,
system.xqueue = { 'interface': XqueueInterface object,
'callback_url': Per-StudentModule callback URL
'construct_callback': Per-StudentModule callback URL
where results are posted (string),
constructor, defaults to using 'score_update'
as the correct dispatch (function),
'default_queuename': Default queuename to submit request (string)
'default_queuename': Default queuename to submit request (string)
}
}
...
@@ -1242,7 +1282,7 @@ class CodeResponse(LoncapaResponse):
...
@@ -1242,7 +1282,7 @@ class CodeResponse(LoncapaResponse):
"""
"""
response_tag
=
'coderesponse'
response_tag
=
'coderesponse'
allowed_inputfields
=
[
'textbox'
,
'filesubmission'
]
allowed_inputfields
=
[
'textbox'
,
'filesubmission'
,
'matlabinput'
]
max_inputfields
=
1
max_inputfields
=
1
def
setup_response
(
self
):
def
setup_response
(
self
):
...
@@ -1263,7 +1303,8 @@ class CodeResponse(LoncapaResponse):
...
@@ -1263,7 +1303,8 @@ class CodeResponse(LoncapaResponse):
self
.
queue_name
=
xml
.
get
(
'queuename'
,
default_queuename
)
self
.
queue_name
=
xml
.
get
(
'queuename'
,
default_queuename
)
# VS[compat]:
# 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'
)
codeparam
=
self
.
xml
.
find
(
'codeparam'
)
if
codeparam
is
None
:
if
codeparam
is
None
:
self
.
_parse_externalresponse_xml
()
self
.
_parse_externalresponse_xml
()
...
@@ -1277,12 +1318,14 @@ class CodeResponse(LoncapaResponse):
...
@@ -1277,12 +1318,14 @@ class CodeResponse(LoncapaResponse):
self.answer (an answer to display to the student in the LMS)
self.answer (an answer to display to the student in the LMS)
self.payload
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
=
codeparam
.
find
(
'grader_payload'
)
grader_payload
=
grader_payload
.
text
if
grader_payload
is
not
None
else
''
grader_payload
=
grader_payload
.
text
if
grader_payload
is
not
None
else
''
self
.
payload
=
{
'grader_payload'
:
grader_payload
}
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'
,
self
.
answer
=
find_with_default
(
codeparam
,
'answer_display'
,
'No answer provided.'
)
'No answer provided.'
)
...
@@ -1304,8 +1347,10 @@ class CodeResponse(LoncapaResponse):
...
@@ -1304,8 +1347,10 @@ class CodeResponse(LoncapaResponse):
else
:
# no <answer> stanza; get code from <script>
else
:
# no <answer> stanza; get code from <script>
code
=
self
.
context
[
'script_code'
]
code
=
self
.
context
[
'script_code'
]
if
not
code
:
if
not
code
:
msg
=
'
%
s: Missing answer script code for coderesponse'
%
unicode
(
self
)
msg
=
'
%
s: Missing answer script code for coderesponse'
%
unicode
(
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
self
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
raise
LoncapaProblemError
(
msg
)
tests
=
self
.
xml
.
get
(
'tests'
)
tests
=
self
.
xml
.
get
(
'tests'
)
...
@@ -1320,7 +1365,8 @@ class CodeResponse(LoncapaResponse):
...
@@ -1320,7 +1365,8 @@ class CodeResponse(LoncapaResponse):
try
:
try
:
exec
(
code
,
penv
,
penv
)
exec
(
code
,
penv
,
penv
)
except
Exception
as
err
:
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
)
raise
Exception
(
err
)
try
:
try
:
self
.
answer
=
penv
[
'answer'
]
self
.
answer
=
penv
[
'answer'
]
...
@@ -1333,7 +1379,7 @@ class CodeResponse(LoncapaResponse):
...
@@ -1333,7 +1379,7 @@ class CodeResponse(LoncapaResponse):
# Finally, make the ExternalResponse input XML format conform to the generic
# Finally, make the ExternalResponse input XML format conform to the generic
# exteral grader interface
# exteral grader interface
# The XML tagging of grader_payload is pyxserver-specific
# The XML tagging of grader_payload is pyxserver-specific
grader_payload
=
'<pyxserver>'
grader_payload
=
'<pyxserver>'
grader_payload
+=
'<tests>'
+
tests
+
'</tests>
\n
'
grader_payload
+=
'<tests>'
+
tests
+
'</tests>
\n
'
grader_payload
+=
'<processor>'
+
code
+
'</processor>'
grader_payload
+=
'<processor>'
+
code
+
'</processor>'
grader_payload
+=
'</pyxserver>'
grader_payload
+=
'</pyxserver>'
...
@@ -1346,14 +1392,14 @@ class CodeResponse(LoncapaResponse):
...
@@ -1346,14 +1392,14 @@ class CodeResponse(LoncapaResponse):
except
Exception
as
err
:
except
Exception
as
err
:
log
.
error
(
'Error in CodeResponse
%
s: cannot get student answer for
%
s;'
log
.
error
(
'Error in CodeResponse
%
s: cannot get student answer for
%
s;'
' student_answers=
%
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
)
raise
Exception
(
err
)
# We do not support xqueue within Studio.
# We do not support xqueue within Studio.
if
self
.
system
.
xqueue
is
None
:
if
self
.
system
.
xqueue
is
None
:
cmap
=
CorrectMap
()
cmap
=
CorrectMap
()
cmap
.
set
(
self
.
answer_id
,
queuestate
=
None
,
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
return
cmap
# Prepare xqueue request
# Prepare xqueue request
...
@@ -1368,9 +1414,11 @@ class CodeResponse(LoncapaResponse):
...
@@ -1368,9 +1414,11 @@ class CodeResponse(LoncapaResponse):
queuekey
=
xqueue_interface
.
make_hashkey
(
str
(
self
.
system
.
seed
)
+
qtime
+
queuekey
=
xqueue_interface
.
make_hashkey
(
str
(
self
.
system
.
seed
)
+
qtime
+
anonymous_student_id
+
anonymous_student_id
+
self
.
answer_id
)
self
.
answer_id
)
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
self
.
system
.
xqueue
[
'callback_url'
],
callback_url
=
self
.
system
.
xqueue
[
'construct_callback'
]()
lms_key
=
queuekey
,
xheader
=
xqueue_interface
.
make_xheader
(
queue_name
=
self
.
queue_name
)
lms_callback_url
=
callback_url
,
lms_key
=
queuekey
,
queue_name
=
self
.
queue_name
)
# Generate body
# Generate body
if
is_list_of_files
(
submission
):
if
is_list_of_files
(
submission
):
...
@@ -1381,13 +1429,16 @@ class CodeResponse(LoncapaResponse):
...
@@ -1381,13 +1429,16 @@ class CodeResponse(LoncapaResponse):
contents
=
self
.
payload
.
copy
()
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
,
student_info
=
{
'anonymous_student_id'
:
anonymous_student_id
,
'submission_time'
:
qtime
,
'submission_time'
:
qtime
,
}
}
contents
.
update
({
'student_info'
:
json
.
dumps
(
student_info
)})
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
):
if
is_list_of_files
(
submission
):
# TODO: Is there any information we want to send here?
# TODO: Is there any information we want to send here?
contents
.
update
({
'student_response'
:
''
})
contents
.
update
({
'student_response'
:
''
})
...
@@ -1415,13 +1466,15 @@ class CodeResponse(LoncapaResponse):
...
@@ -1415,13 +1466,15 @@ class CodeResponse(LoncapaResponse):
# 2) Frontend: correctness='incomplete' eventually trickles down
# 2) Frontend: correctness='incomplete' eventually trickles down
# through inputtypes.textbox and .filesubmission to inform the
# through inputtypes.textbox and .filesubmission to inform the
# browser to poll the LMS
# 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
return
cmap
def
update_score
(
self
,
score_msg
,
oldcmap
,
queuekey
):
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
:
if
not
valid_score_msg
:
oldcmap
.
set
(
self
.
answer_id
,
oldcmap
.
set
(
self
.
answer_id
,
msg
=
'Invalid grader reply. Please contact the course staff.'
)
msg
=
'Invalid grader reply. Please contact the course staff.'
)
...
@@ -1433,14 +1486,16 @@ class CodeResponse(LoncapaResponse):
...
@@ -1433,14 +1486,16 @@ class CodeResponse(LoncapaResponse):
self
.
context
[
'correct'
]
=
correctness
self
.
context
[
'correct'
]
=
correctness
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
# 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
):
if
oldcmap
.
is_right_queuekey
(
self
.
answer_id
,
queuekey
):
# Sanity check on returned points
# Sanity check on returned points
if
points
<
0
:
if
points
<
0
:
points
=
0
points
=
0
# Queuestate is consumed
# Queuestate is consumed
oldcmap
.
set
(
self
.
answer_id
,
npoints
=
points
,
correctness
=
correctness
,
oldcmap
.
set
(
msg
=
msg
.
replace
(
' '
,
' '
),
queuestate
=
None
)
self
.
answer_id
,
npoints
=
points
,
correctness
=
correctness
,
msg
=
msg
.
replace
(
' '
,
' '
),
queuestate
=
None
)
else
:
else
:
log
.
debug
(
'CodeResponse: queuekey
%
s does not match for answer_id=
%
s.'
%
log
.
debug
(
'CodeResponse: queuekey
%
s does not match for answer_id=
%
s.'
%
(
queuekey
,
self
.
answer_id
))
(
queuekey
,
self
.
answer_id
))
...
@@ -1560,15 +1615,18 @@ main()
...
@@ -1560,15 +1615,18 @@ main()
if
answer
is
not
None
:
if
answer
is
not
None
:
answer_src
=
answer
.
get
(
'src'
)
answer_src
=
answer
.
get
(
'src'
)
if
answer_src
is
not
None
:
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
:
else
:
self
.
code
=
answer
.
text
self
.
code
=
answer
.
text
else
:
else
:
# no <answer> stanza; get code from <script>
# no <answer> stanza; get code from <script>
self
.
code
=
self
.
context
[
'script_code'
]
self
.
code
=
self
.
context
[
'script_code'
]
if
not
self
.
code
:
if
not
self
.
code
:
msg
=
'
%
s: Missing answer script code for externalresponse'
%
unicode
(
self
)
msg
=
'
%
s: Missing answer script code for externalresponse'
%
unicode
(
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
self
)
msg
+=
"
\n
See XML source line
%
s"
%
getattr
(
self
.
xml
,
'sourceline'
,
'<unavailable>'
)
raise
LoncapaProblemError
(
msg
)
raise
LoncapaProblemError
(
msg
)
self
.
tests
=
xml
.
get
(
'tests'
)
self
.
tests
=
xml
.
get
(
'tests'
)
...
@@ -1591,10 +1649,12 @@ main()
...
@@ -1591,10 +1649,12 @@ main()
payload
.
update
(
extra_payload
)
payload
.
update
(
extra_payload
)
try
:
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
)
r
=
requests
.
post
(
self
.
url
,
data
=
payload
)
except
Exception
as
err
:
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
)
log
.
error
(
msg
)
raise
Exception
(
msg
)
raise
Exception
(
msg
)
...
@@ -1602,13 +1662,15 @@ main()
...
@@ -1602,13 +1662,15 @@ main()
log
.
info
(
'response =
%
s'
%
r
.
text
)
log
.
info
(
'response =
%
s'
%
r
.
text
)
if
(
not
r
.
text
)
or
(
not
r
.
text
.
strip
()):
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
:
try
:
# response is XML; parse it
# response is XML; parse it
rxml
=
etree
.
fromstring
(
r
.
text
)
rxml
=
etree
.
fromstring
(
r
.
text
)
except
Exception
as
err
:
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
)
log
.
error
(
msg
)
raise
Exception
(
msg
)
raise
Exception
(
msg
)
...
@@ -1633,7 +1695,8 @@ main()
...
@@ -1633,7 +1695,8 @@ main()
except
Exception
as
err
:
except
Exception
as
err
:
log
.
error
(
'Error
%
s'
%
err
)
log
.
error
(
'Error
%
s'
%
err
)
if
self
.
system
.
DEBUG
:
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
(
cmap
.
set_property
(
self
.
answer_ids
[
0
],
'msg'
,
self
.
answer_ids
[
0
],
'msg'
,
'<span class="inline-error">
%
s</span>'
%
str
(
err
)
.
replace
(
'<'
,
'<'
))
'<span class="inline-error">
%
s</span>'
%
str
(
err
)
.
replace
(
'<'
,
'<'
))
...
@@ -1650,7 +1713,8 @@ main()
...
@@ -1650,7 +1713,8 @@ main()
# create CorrectMap
# create CorrectMap
for
key
in
idset
:
for
key
in
idset
:
idx
=
idset
.
index
(
key
)
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
)
cmap
.
set
(
key
,
self
.
context
[
'correct'
][
idx
],
msg
=
msg
)
return
cmap
return
cmap
...
@@ -1665,7 +1729,8 @@ main()
...
@@ -1665,7 +1729,8 @@ main()
except
Exception
as
err
:
except
Exception
as
err
:
log
.
error
(
'Error
%
s'
%
err
)
log
.
error
(
'Error
%
s'
%
err
)
if
self
.
system
.
DEBUG
:
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
=
[
''
]
*
len
(
self
.
answer_ids
)
exans
[
0
]
=
msg
exans
[
0
]
=
msg
...
@@ -1712,8 +1777,9 @@ class FormulaResponse(LoncapaResponse):
...
@@ -1712,8 +1777,9 @@ class FormulaResponse(LoncapaResponse):
self
.
correct_answer
=
contextualize_text
(
xml
.
get
(
'answer'
),
context
)
self
.
correct_answer
=
contextualize_text
(
xml
.
get
(
'answer'
),
context
)
self
.
samples
=
contextualize_text
(
xml
.
get
(
'samples'
),
context
)
self
.
samples
=
contextualize_text
(
xml
.
get
(
'samples'
),
context
)
try
:
try
:
self
.
tolerance_xml
=
xml
.
xpath
(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
self
.
tolerance_xml
=
xml
.
xpath
(
id
=
xml
.
get
(
'id'
))[
0
]
'//*[@id=$id]//responseparam[@type="tolerance"]/@default'
,
id
=
xml
.
get
(
'id'
))[
0
]
self
.
tolerance
=
contextualize_text
(
self
.
tolerance_xml
,
context
)
self
.
tolerance
=
contextualize_text
(
self
.
tolerance_xml
,
context
)
except
Exception
:
except
Exception
:
self
.
tolerance
=
'0.00001'
self
.
tolerance
=
'0.00001'
...
@@ -1735,14 +1801,15 @@ class FormulaResponse(LoncapaResponse):
...
@@ -1735,14 +1801,15 @@ class FormulaResponse(LoncapaResponse):
def
get_score
(
self
,
student_answers
):
def
get_score
(
self
,
student_answers
):
given
=
student_answers
[
self
.
answer_id
]
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
)
return
CorrectMap
(
self
.
answer_id
,
correctness
)
def
check_formula
(
self
,
expected
,
given
,
samples
):
def
check_formula
(
self
,
expected
,
given
,
samples
):
variables
=
samples
.
split
(
'@'
)[
0
]
.
split
(
','
)
variables
=
samples
.
split
(
'@'
)[
0
]
.
split
(
','
)
numsamples
=
int
(
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
1
])
numsamples
=
int
(
samples
.
split
(
'@'
)[
1
]
.
split
(
'#'
)[
1
])
sranges
=
zip
(
*
map
(
lambda
x
:
map
(
float
,
x
.
split
(
","
)),
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
))
ranges
=
dict
(
zip
(
variables
,
sranges
))
for
i
in
range
(
numsamples
):
for
i
in
range
(
numsamples
):
...
@@ -1753,22 +1820,26 @@ class FormulaResponse(LoncapaResponse):
...
@@ -1753,22 +1820,26 @@ class FormulaResponse(LoncapaResponse):
value
=
random
.
uniform
(
*
ranges
[
var
])
value
=
random
.
uniform
(
*
ranges
[
var
])
instructor_variables
[
str
(
var
)]
=
value
instructor_variables
[
str
(
var
)]
=
value
student_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
(),
instructor_result
=
evaluator
(
instructor_variables
,
dict
(),
expected
,
cs
=
self
.
case_sensitive
)
expected
,
cs
=
self
.
case_sensitive
)
try
:
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
,
student_result
=
evaluator
(
student_variables
,
dict
(),
dict
(),
given
,
given
,
cs
=
self
.
case_sensitive
)
cs
=
self
.
case_sensitive
)
except
UndefinedVariable
as
uv
:
except
UndefinedVariable
as
uv
:
log
.
debug
(
'formularesponse: undefined variable in given=
%
s'
%
given
)
log
.
debug
(
raise
StudentInputError
(
"Invalid input: "
+
uv
.
message
+
" not permitted in answer"
)
'formularesponse: undefined variable in given=
%
s'
%
given
)
raise
StudentInputError
(
"Invalid input: "
+
uv
.
message
+
" not permitted in answer"
)
except
Exception
as
err
:
except
Exception
as
err
:
#traceback.print_exc()
#
traceback.print_exc()
log
.
debug
(
'formularesponse: error
%
s in formula'
%
err
)
log
.
debug
(
'formularesponse: error
%
s in formula'
%
err
)
raise
StudentInputError
(
"Invalid input: Could not parse '
%
s' as a formula"
%
\
raise
StudentInputError
(
"Invalid input: Could not parse '
%
s' as a formula"
%
cgi
.
escape
(
given
))
cgi
.
escape
(
given
))
if
numpy
.
isnan
(
student_result
)
or
numpy
.
isinf
(
student_result
):
if
numpy
.
isnan
(
student_result
)
or
numpy
.
isinf
(
student_result
):
return
"incorrect"
return
"incorrect"
...
@@ -1792,9 +1863,11 @@ class FormulaResponse(LoncapaResponse):
...
@@ -1792,9 +1863,11 @@ class FormulaResponse(LoncapaResponse):
for
hxml
in
hxml_set
:
for
hxml
in
hxml_set
:
samples
=
hxml
.
get
(
'samples'
)
samples
=
hxml
.
get
(
'samples'
)
name
=
hxml
.
get
(
'name'
)
name
=
hxml
.
get
(
'name'
)
correct_answer
=
contextualize_text
(
hxml
.
get
(
'answer'
),
self
.
context
)
correct_answer
=
contextualize_text
(
hxml
.
get
(
'answer'
),
self
.
context
)
try
:
try
:
correctness
=
self
.
check_formula
(
correct_answer
,
given
,
samples
)
correctness
=
self
.
check_formula
(
correct_answer
,
given
,
samples
)
except
Exception
:
except
Exception
:
correctness
=
'incorrect'
correctness
=
'incorrect'
if
correctness
==
'correct'
:
if
correctness
==
'correct'
:
...
@@ -1825,11 +1898,13 @@ class SchematicResponse(LoncapaResponse):
...
@@ -1825,11 +1898,13 @@ class SchematicResponse(LoncapaResponse):
def
get_score
(
self
,
student_answers
):
def
get_score
(
self
,
student_answers
):
from
capa_problem
import
global_context
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
})
self
.
context
.
update
({
'submission'
:
submission
})
exec
self
.
code
in
global_context
,
self
.
context
exec
self
.
code
in
global_context
,
self
.
context
cmap
=
CorrectMap
()
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
return
cmap
def
get_answers
(
self
):
def
get_answers
(
self
):
...
@@ -1891,12 +1966,14 @@ class ImageResponse(LoncapaResponse):
...
@@ -1891,12 +1966,14 @@ class ImageResponse(LoncapaResponse):
expectedset
=
self
.
get_answers
()
expectedset
=
self
.
get_answers
()
for
aid
in
self
.
answer_ids
:
# loop through IDs of <imageinput>
for
aid
in
self
.
answer_ids
:
# loop through IDs of <imageinput>
# fields in our stanza
# 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'
)
correct_map
.
set
(
aid
,
'incorrect'
)
if
not
given
:
# No answer to parse. Mark as incorrect and move on
if
not
given
:
# No answer to parse. Mark as incorrect and move on
continue
continue
# parse given answer
# 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
:
if
not
m
:
raise
Exception
(
'[capamodule.capa.responsetypes.imageinput] '
raise
Exception
(
'[capamodule.capa.responsetypes.imageinput] '
'error grading
%
s (input=
%
s)'
%
(
aid
,
given
))
'error grading
%
s (input=
%
s)'
%
(
aid
,
given
))
...
@@ -1904,20 +1981,24 @@ class ImageResponse(LoncapaResponse):
...
@@ -1904,20 +1981,24 @@ class ImageResponse(LoncapaResponse):
rectangles
,
regions
=
expectedset
rectangles
,
regions
=
expectedset
if
rectangles
[
aid
]:
# rectangles part - for backward compatibility
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
(
';'
)
solution_rectangles
=
rectangles
[
aid
]
.
split
(
';'
)
for
solution_rectangle
in
solution_rectangles
:
for
solution_rectangle
in
solution_rectangles
:
# parse expected answer
# parse expected answer
# TODO: Compile regexp on file load
# TODO: Compile regexp on file load
m
=
re
.
match
(
'[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]-[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]'
,
m
=
re
.
match
(
solution_rectangle
.
strip
()
.
replace
(
' '
,
''
))
'[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]-[
\
(
\
[]([0-9]+),([0-9]+)[
\
)
\
]]'
,
solution_rectangle
.
strip
()
.
replace
(
' '
,
''
))
if
not
m
:
if
not
m
:
msg
=
'Error in problem specification! cannot parse rectangle in
%
s'
%
(
msg
=
'Error in problem specification! cannot parse rectangle in
%
s'
%
(
etree
.
tostring
(
self
.
ielements
[
aid
],
pretty_print
=
True
))
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
()]
(
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
):
if
(
llx
<=
gx
<=
urx
)
and
(
lly
<=
gy
<=
ury
):
correct_map
.
set
(
aid
,
'correct'
)
correct_map
.
set
(
aid
,
'correct'
)
break
break
...
@@ -1938,10 +2019,13 @@ class ImageResponse(LoncapaResponse):
...
@@ -1938,10 +2019,13 @@ class ImageResponse(LoncapaResponse):
return
correct_map
return
correct_map
def
get_answers
(
self
):
def
get_answers
(
self
):
return
(
dict
([(
ie
.
get
(
'id'
),
ie
.
get
(
'rectangle'
))
for
ie
in
self
.
ielements
]),
return
(
dict
([(
ie
.
get
(
'id'
),
ie
.
get
(
'regions'
))
for
ie
in
self
.
ielements
]))
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
):
class
AnnotationResponse
(
LoncapaResponse
):
'''
'''
Checking of annotation responses.
Checking of annotation responses.
...
@@ -1952,7 +2036,8 @@ class AnnotationResponse(LoncapaResponse):
...
@@ -1952,7 +2036,8 @@ class AnnotationResponse(LoncapaResponse):
response_tag
=
'annotationresponse'
response_tag
=
'annotationresponse'
allowed_inputfields
=
[
'annotationinput'
]
allowed_inputfields
=
[
'annotationinput'
]
max_inputfields
=
1
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
):
def
setup_response
(
self
):
xml
=
self
.
xml
xml
=
self
.
xml
self
.
scoring_map
=
self
.
_get_scoring_map
()
self
.
scoring_map
=
self
.
_get_scoring_map
()
...
@@ -1966,7 +2051,8 @@ class AnnotationResponse(LoncapaResponse):
...
@@ -1966,7 +2051,8 @@ class AnnotationResponse(LoncapaResponse):
student_option
=
self
.
_get_submitted_option_id
(
student_answer
)
student_option
=
self
.
_get_submitted_option_id
(
student_answer
)
scoring
=
self
.
scoring_map
[
self
.
answer_id
]
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
)
(
correctness
,
points
)
=
(
'incorrect'
,
None
)
if
is_valid
:
if
is_valid
:
...
@@ -1981,7 +2067,7 @@ class AnnotationResponse(LoncapaResponse):
...
@@ -1981,7 +2067,7 @@ class AnnotationResponse(LoncapaResponse):
def
_get_scoring_map
(
self
):
def
_get_scoring_map
(
self
):
''' Returns a dict of option->scoring for each input. '''
''' Returns a dict of option->scoring for each input. '''
scoring
=
self
.
default_scoring
scoring
=
self
.
default_scoring
choices
=
dict
([(
choice
,
choice
)
for
choice
in
scoring
])
choices
=
dict
([(
choice
,
choice
)
for
choice
in
scoring
])
scoring_map
=
{}
scoring_map
=
{}
for
inputfield
in
self
.
inputfields
:
for
inputfield
in
self
.
inputfields
:
...
@@ -1998,9 +2084,11 @@ class AnnotationResponse(LoncapaResponse):
...
@@ -1998,9 +2084,11 @@ class AnnotationResponse(LoncapaResponse):
''' Returns a dict of answers for each input.'''
''' Returns a dict of answers for each input.'''
answer_map
=
{}
answer_map
=
{}
for
inputfield
in
self
.
inputfields
:
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
:
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
return
answer_map
def
_get_max_points
(
self
):
def
_get_max_points
(
self
):
...
@@ -2016,7 +2104,7 @@ class AnnotationResponse(LoncapaResponse):
...
@@ -2016,7 +2104,7 @@ class AnnotationResponse(LoncapaResponse):
'id'
:
index
,
'id'
:
index
,
'description'
:
option
.
text
,
'description'
:
option
.
text
,
'choice'
:
option
.
get
(
'choice'
)
'choice'
:
option
.
get
(
'choice'
)
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
}
for
(
index
,
option
)
in
enumerate
(
elements
)
]
def
_find_option_with_choice
(
self
,
inputfield
,
choice
):
def
_find_option_with_choice
(
self
,
inputfield
,
choice
):
''' Returns the option with the given choice value, otherwise None. '''
''' 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
...
@@ -2,7 +2,7 @@ import fs
import
fs.osfs
import
fs.osfs
import
os
import
os
from
mock
import
Mock
from
mock
import
Mock
,
MagicMock
import
xml.sax.saxutils
as
saxutils
import
xml.sax.saxutils
as
saxutils
...
@@ -16,6 +16,11 @@ def tst_render_template(template, context):
...
@@ -16,6 +16,11 @@ def tst_render_template(template, context):
"""
"""
return
'<div>{0}</div>'
.
format
(
saxutils
.
escape
(
repr
(
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
(
test_system
=
Mock
(
ajax_url
=
'courses/course_id/modx/a_location'
,
ajax_url
=
'courses/course_id/modx/a_location'
,
...
@@ -26,7 +31,7 @@ test_system = Mock(
...
@@ -26,7 +31,7 @@ test_system = Mock(
user
=
Mock
(),
user
=
Mock
(),
filestore
=
fs
.
osfs
.
OSFS
(
os
.
path
.
join
(
TEST_DIR
,
"test_files"
)),
filestore
=
fs
.
osfs
.
OSFS
(
os
.
path
.
join
(
TEST_DIR
,
"test_files"
)),
debug
=
True
,
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"
),
node_path
=
os
.
environ
.
get
(
"NODE_PATH"
,
"/usr/local/lib/node_modules"
),
anonymous_student_id
=
'student'
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
...
@@ -23,6 +23,7 @@ import xml.sax.saxutils as saxutils
from
.
import
test_system
from
.
import
test_system
from
capa
import
inputtypes
from
capa
import
inputtypes
from
mock
import
ANY
# just a handy shortcut
# just a handy shortcut
lookup_tag
=
inputtypes
.
registry
.
get_class_for_tag
lookup_tag
=
inputtypes
.
registry
.
get_class_for_tag
...
@@ -300,6 +301,98 @@ class CodeInputTest(unittest.TestCase):
...
@@ -300,6 +301,98 @@ class CodeInputTest(unittest.TestCase):
self
.
assertEqual
(
context
,
expected
)
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
):
class
SchematicTest
(
unittest
.
TestCase
):
'''
'''
...
...
common/lib/xmodule/xmodule/capa_module.py
View file @
f0e1b477
...
@@ -93,6 +93,7 @@ class CapaFields(object):
...
@@ -93,6 +93,7 @@ class CapaFields(object):
rerandomize
=
Randomization
(
help
=
"When to rerandomize the problem"
,
default
=
"always"
,
scope
=
Scope
.
settings
)
rerandomize
=
Randomization
(
help
=
"When to rerandomize the problem"
,
default
=
"always"
,
scope
=
Scope
.
settings
)
data
=
String
(
help
=
"XML data for the problem"
,
scope
=
Scope
.
content
)
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
=
{})
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
)
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
)
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
)
display_name
=
String
(
help
=
"Display name for this module"
,
scope
=
Scope
.
settings
)
...
@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
...
@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
'done'
:
self
.
done
,
'done'
:
self
.
done
,
'correct_map'
:
self
.
correct_map
,
'correct_map'
:
self
.
correct_map
,
'student_answers'
:
self
.
student_answers
,
'student_answers'
:
self
.
student_answers
,
'input_state'
:
self
.
input_state
,
'seed'
:
self
.
seed
,
'seed'
:
self
.
seed
,
}
}
...
@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
...
@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
lcp_state
=
self
.
lcp
.
get_state
()
lcp_state
=
self
.
lcp
.
get_state
()
self
.
done
=
lcp_state
[
'done'
]
self
.
done
=
lcp_state
[
'done'
]
self
.
correct_map
=
lcp_state
[
'correct_map'
]
self
.
correct_map
=
lcp_state
[
'correct_map'
]
self
.
input_state
=
lcp_state
[
'input_state'
]
self
.
student_answers
=
lcp_state
[
'student_answers'
]
self
.
student_answers
=
lcp_state
[
'student_answers'
]
self
.
seed
=
lcp_state
[
'seed'
]
self
.
seed
=
lcp_state
[
'seed'
]
...
@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
...
@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
'problem_save'
:
self
.
save_problem
,
'problem_save'
:
self
.
save_problem
,
'problem_show'
:
self
.
get_answer
,
'problem_show'
:
self
.
get_answer
,
'score_update'
:
self
.
update_score
,
'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
:
if
dispatch
not
in
handlers
:
...
@@ -537,6 +541,43 @@ class CapaModule(CapaFields, XModule):
...
@@ -537,6 +541,43 @@ class CapaModule(CapaFields, XModule):
return
dict
()
# No AJAX return is needed
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
):
def
get_answer
(
self
,
get
):
'''
'''
For the "show answer" button.
For the "show answer" button.
...
...
common/lib/xmodule/xmodule/js/src/capa/display.coffee
View file @
f0e1b477
...
@@ -41,6 +41,11 @@ class @Problem
...
@@ -41,6 +41,11 @@ class @Problem
@
el
.
attr
progress
:
response
.
progress_status
@
el
.
attr
progress
:
response
.
progress_status
@
el
.
trigger
(
'progressChanged'
)
@
el
.
trigger
(
'progressChanged'
)
forceUpdate
:
(
response
)
=>
@
el
.
attr
progress
:
response
.
progress_status
@
el
.
trigger
(
'progressChanged'
)
queueing
:
=>
queueing
:
=>
@
queued_items
=
@
$
(
".xqueue"
)
@
queued_items
=
@
$
(
".xqueue"
)
@
num_queued_items
=
@
queued_items
.
length
@
num_queued_items
=
@
queued_items
.
length
...
@@ -71,6 +76,7 @@ class @Problem
...
@@ -71,6 +76,7 @@ class @Problem
@
num_queued_items
=
@
new_queued_items
.
length
@
num_queued_items
=
@
new_queued_items
.
length
if
@
num_queued_items
==
0
if
@
num_queued_items
==
0
@
forceUpdate
response
delete
window
.
queuePollerID
delete
window
.
queuePollerID
else
else
# TODO: Some logic to dynamically adjust polling rate based on queuelen
# 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):
...
@@ -174,7 +174,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
str
(
len
(
self
.
child_history
)))
str
(
len
(
self
.
child_history
)))
xheader
=
xqueue_interface
.
make_xheader
(
xheader
=
xqueue_interface
.
make_xheader
(
lms_callback_url
=
system
.
xqueue
[
'c
allback_url'
]
,
lms_callback_url
=
system
.
xqueue
[
'c
onstruct_callback'
]()
,
lms_key
=
queuekey
,
lms_key
=
queuekey
,
queue_name
=
self
.
message_queue_name
queue_name
=
self
.
message_queue_name
)
)
...
@@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
...
@@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
anonymous_student_id
+
anonymous_student_id
+
str
(
len
(
self
.
child_history
)))
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
,
lms_key
=
queuekey
,
queue_name
=
self
.
queue_name
)
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):
...
@@ -183,7 +183,10 @@ class OpenEndedModuleTest(unittest.TestCase):
self
.
test_system
.
location
=
self
.
location
self
.
test_system
.
location
=
self
.
location
self
.
mock_xqueue
=
MagicMock
()
self
.
mock_xqueue
=
MagicMock
()
self
.
mock_xqueue
.
send_to_queue
.
return_value
=
(
None
,
"Message"
)
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
}
'waittime'
:
1
}
self
.
openendedmodule
=
OpenEndedModule
(
self
.
test_system
,
self
.
location
,
self
.
openendedmodule
=
OpenEndedModule
(
self
.
test_system
,
self
.
location
,
self
.
definition
,
self
.
descriptor
,
self
.
static_data
,
self
.
metadata
)
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
...
@@ -181,12 +181,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
host
=
request
.
get_host
(),
host
=
request
.
get_host
(),
proto
=
request
.
META
.
get
(
'HTTP_X_FORWARDED_PROTO'
,
'https'
if
request
.
is_secure
()
else
'http'
)
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
,
def
make_xqueue_callback
(
dispatch
=
'score_update'
):
userid
=
str
(
user
.
id
),
# Fully qualified callback URL for external queueing system
id
=
descriptor
.
location
.
url
(),
xqueue_callback_url
=
'{proto}://{host}'
.
format
(
dispatch
=
'score_update'
),
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
# Default queuename is course-specific and is derived from the course that
# contains the current module.
# contains the current module.
...
@@ -194,7 +203,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
...
@@ -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_default_queuename
=
descriptor
.
location
.
org
+
'-'
+
descriptor
.
location
.
course
xqueue
=
{
'interface'
:
xqueue_interface
,
xqueue
=
{
'interface'
:
xqueue_interface
,
'c
allback_url'
:
xqueue_callback_url
,
'c
onstruct_callback'
:
make_xqueue_callback
,
'default_queuename'
:
xqueue_default_queuename
.
replace
(
' '
,
'_'
),
'default_queuename'
:
xqueue_default_queuename
.
replace
(
' '
,
'_'
),
'waittime'
:
settings
.
XQUEUE_WAITTIME_BETWEEN_REQUESTS
'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