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
46ae2f9c
Commit
46ae2f9c
authored
Jun 27, 2013
by
RobertMarks
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added support for a new problem type: ChoicetextResponse
parent
c789642e
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
1544 additions
and
4 deletions
+1544
-4
AUTHORS
+1
-0
CHANGELOG.rst
+4
-1
common/lib/capa/capa/inputtypes.py
+208
-0
common/lib/capa/capa/responsetypes.py
+331
-1
common/lib/capa/capa/templates/choicetext.html
+76
-0
common/lib/capa/capa/tests/response_xml_factory.py
+106
-0
common/lib/capa/capa/tests/test_input_templates.py
+167
-0
common/lib/capa/capa/tests/test_inputtypes.py
+91
-0
common/lib/capa/capa/tests/test_responsetypes.py
+283
-0
common/lib/xmodule/xmodule/capa_module.py
+18
-1
common/lib/xmodule/xmodule/css/capa/display.scss
+28
-0
common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
+52
-0
common/lib/xmodule/xmodule/js/src/capa/display.coffee
+12
-0
common/static/js/capa/choicetextinput.js
+75
-0
lms/djangoapps/courseware/features/problems.feature
+10
-0
lms/djangoapps/courseware/features/problems_setup.py
+82
-1
No files found.
AUTHORS
View file @
46ae2f9c
...
@@ -81,3 +81,4 @@ Felix Sun <felixsun@mit.edu>
...
@@ -81,3 +81,4 @@ Felix Sun <felixsun@mit.edu>
Adam Palay <adam@edx.org>
Adam Palay <adam@edx.org>
Ian Hoover <ihoover@edx.org>
Ian Hoover <ihoover@edx.org>
Mukul Goyal <miki@edx.org>
Mukul Goyal <miki@edx.org>
Robert Marks <rmarks@edx.org>
CHANGELOG.rst
View file @
46ae2f9c
...
@@ -5,6 +5,7 @@ These are notable changes in edx-platform. This is a rolling list of changes,
...
@@ -5,6 +5,7 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
the top. Include a label indicating the component affected.
Common: Added *experimental* support for jsinput type.
Common: Added *experimental* support for jsinput type.
Common: Added setting to specify Celery Broker vhost
Common: Added setting to specify Celery Broker vhost
...
@@ -21,6 +22,8 @@ Studio: Added support for uploading and managing PDF textbooks
...
@@ -21,6 +22,8 @@ Studio: Added support for uploading and managing PDF textbooks
Common: Student information is now passed to the tracking log via POST instead of GET.
Common: Student information is now passed to the tracking log via POST instead of GET.
Blades: Added functionality and tests for new capa input type: choicetextresponse.
Common: Add tests for documentation generation to test suite
Common: Add tests for documentation generation to test suite
Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems
Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems
...
@@ -43,7 +46,7 @@ history of background tasks for a given problem and student.
...
@@ -43,7 +46,7 @@ history of background tasks for a given problem and student.
Blades: Small UX fix on capa multiple-choice problems. Make labels only
Blades: Small UX fix on capa multiple-choice problems. Make labels only
as wide as the text to reduce accidental choice selections.
as wide as the text to reduce accidental choice selections.
Studio:
Studio:
- use xblock field defaults to initialize all new instances' fields and
- use xblock field defaults to initialize all new instances' fields and
only use templates as override samples.
only use templates as override samples.
- create new instances via in memory create_xmodule and related methods rather
- create new instances via in memory create_xmodule and related methods rather
...
...
common/lib/capa/capa/inputtypes.py
View file @
46ae2f9c
...
@@ -1368,3 +1368,211 @@ class AnnotationInput(InputTypeBase):
...
@@ -1368,3 +1368,211 @@ class AnnotationInput(InputTypeBase):
return
extra_context
return
extra_context
registry
.
register
(
AnnotationInput
)
registry
.
register
(
AnnotationInput
)
class
ChoiceTextGroup
(
InputTypeBase
):
"""
Groups of radiobutton/checkboxes with text inputs.
Allows for a "not enough information" option to be added
to problems with numerical answers.
Examples:
RadioButton problem
<problem>
<startouttext/>
A person rolls a standard die 100 times and records the results.
On the first roll they received a "1". Given this information
select the correct choice and fill in numbers to make it accurate.
<endouttext/>
<choicetextresponse>
<radiotextgroup>
<choice correct="false">The lowest number rolled was:
<decoy_input/> and the highest number rolled was:
<decoy_input/> .</choice>
<choice correct="true">The lowest number rolled was <numtolerance_input answer="1"/>
and there is not enough information to determine the highest number rolled.
</choice>
<choice correct="false">There is not enough information to determine the lowest
number rolled, and the highest number rolled was:
<decoy_input/> .
</choice>
</radiotextgroup>
</choicetextresponse>
</problem>
CheckboxProblem:
<problem>
<startouttext/>
A person randomly selects 100 times, with replacement, from the list of numbers
\
(
\
sqrt{2}
\
) , 2, 3, 4 ,5 ,6
and records the results. The first number they pick is
\
(
\
sqrt{2}
\
) Given this information
select the correct choices and fill in numbers to make them accurate.
<endouttext/>
<choicetextresponse>
<checkboxtextgroup>
<choice correct="true">
The lowest number selected was <numtolerance_input answer="1.4142" tolerance="0.01"/>
</choice>
<choice correct="false">
The highest number selected was <decoy_input/> .
</choice>
<choice correct="true">There is not enough information given to determine the highest number
which was selected.
</choice>
<choice correct="false">There is not enough information given to determine the lowest number
selected.
</choice>
</checkboxtextgroup>
</choicetextresponse>
</problem>
In the preceding examples the <decoy_input/> is used to generate a textinput html element
in the problem's display. Since it is inside of an incorrect choice, no answer given
for it will be correct, and thus specifying an answer for it is not needed.
"""
template
=
"choicetext.html"
tags
=
[
'radiotextgroup'
,
'checkboxtextgroup'
]
def
setup
(
self
):
"""
Performs setup for the initial rendering of the problem.
`self.html_input_type` determines whether this problem is displayed
with radiobuttons or checkboxes
If the initial value of `self.value` is '' change it to {} so that
the template has an empty dictionary to work with.
sets the value of self.choices to be equal to the return value of
`self.extract_choices`
"""
self
.
text_input_values
=
{}
if
self
.
tag
==
'radiotextgroup'
:
self
.
html_input_type
=
"radio"
elif
self
.
tag
==
'checkboxtextgroup'
:
self
.
html_input_type
=
"checkbox"
else
:
raise
Exception
(
"ChoiceGroup: unexpected tag {0}"
.
format
(
self
.
tag
))
if
self
.
value
==
''
:
# Make `value` an empty dictionary, if it currently has an empty
# value. This is necessary because the template expects a
# dictionary.
self
.
value
=
{}
self
.
choices
=
self
.
extract_choices
(
self
.
xml
)
@classmethod
def
get_attributes
(
cls
):
"""
Returns a list of `Attribute` for this problem type
"""
return
[
Attribute
(
"show_correctness"
,
"always"
),
Attribute
(
"submitted_message"
,
"Answer received."
)
]
def
_extra_context
(
self
):
"""
Returns a dictionary of extra content necessary for rendering this InputType.
`input_type` is either 'radio' or 'checkbox' indicating whether the choices for
this problem will have radiobuttons or checkboxes.
"""
return
{
'input_type'
:
self
.
html_input_type
,
'choices'
:
self
.
choices
}
@staticmethod
def
extract_choices
(
element
):
"""
Extracts choices from the xml for this problem type.
If we have xml that is as follows(choice names will have been assigned
by now)
<radiotextgroup>
<choice correct = "true" name ="1_2_1_choiceinput_0bc">
The number
<numtolerance_input name = "1_2_1_choiceinput0_numtolerance_input_0" answer="5"/>
Is the mean of the list.
</choice>
<choice correct = "false" name = "1_2_1_choiceinput_1bc>
False demonstration choice
</choice>
</radiotextgroup>
Choices are used for rendering the problem properly
The function will setup choices as follows:
choices =[
("1_2_1_choiceinput_0bc",
[{'type': 'text', 'contents': "The number", 'tail_text': '',
'value': ''
},
{'type': 'textinput',
'contents': "1_2_1_choiceinput0_numtolerance_input_0",
'tail_text': 'Is the mean of the list',
'value': ''
}
]
),
("1_2_1_choiceinput_1bc",
[{'type': 'text', 'contents': "False demonstration choice",
'tail_text': '',
'value': ''
}
]
)
]
"""
choices
=
[]
for
choice
in
element
:
if
choice
.
tag
!=
'choice'
:
raise
Exception
(
"[capa.inputtypes.extract_choices] Expected a <choice>"
+
"tag; got {0} instead"
.
format
(
choice
.
tag
)
)
components
=
[]
choice_text
=
''
if
choice
.
text
is
not
None
:
choice_text
+=
choice
.
text
# Initialize our dict for the next content
adder
=
{
'type'
:
'text'
,
'contents'
:
choice_text
,
'tail_text'
:
''
,
'value'
:
''
}
components
.
append
(
adder
)
for
elt
in
choice
:
# for elements in the choice e.g. <text> <numtolerance_input>
adder
=
{
'type'
:
'text'
,
'contents'
:
''
,
'tail_text'
:
''
,
'value'
:
''
}
tag_type
=
elt
.
tag
# If the current `elt` is a <numtolerance_input> set the
# `adder`type to 'numtolerance_input', and 'contents' to
# the `elt`'s name.
# Treat decoy_inputs and numtolerance_inputs the same in order
# to prevent students from reading the Html and figuring out
# which inputs are valid
if
tag_type
in
(
'numtolerance_input'
,
'decoy_input'
):
# We set this to textinput, so that we get a textinput html
# element.
adder
[
'type'
]
=
'textinput'
adder
[
'contents'
]
=
elt
.
get
(
'name'
)
else
:
adder
[
'contents'
]
=
elt
.
text
# Add any tail text("is the mean" in the example)
adder
[
'tail_text'
]
=
elt
.
tail
if
elt
.
tail
else
''
components
.
append
(
adder
)
# Add the tuple for the current choice to the list of choices
choices
.
append
((
choice
.
get
(
"name"
),
components
))
return
choices
registry
.
register
(
ChoiceTextGroup
)
common/lib/capa/capa/responsetypes.py
View file @
46ae2f9c
...
@@ -2097,6 +2097,335 @@ class AnnotationResponse(LoncapaResponse):
...
@@ -2097,6 +2097,335 @@ class AnnotationResponse(LoncapaResponse):
return
option_ids
[
0
]
return
option_ids
[
0
]
return
None
return
None
class
ChoiceTextResponse
(
LoncapaResponse
):
"""
Allows for multiple choice responses with text inputs
Desired semantics match those of NumericalResponse and
ChoiceResponse.
"""
response_tag
=
'choicetextresponse'
max_inputfields
=
1
allowed_inputfields
=
[
'choicetextgroup'
,
'checkboxtextgroup'
,
'radiotextgroup'
]
def
setup_response
(
self
):
"""
Sets up three dictionaries for use later:
`correct_choices`: These are the correct binary choices(radio/checkbox)
`correct_inputs`: These are the numerical/string answers for required
inputs.
`answer_values`: This is a dict, keyed by the name of the binary choice
which contains the correct answers for the text inputs separated by
commas e.g. "1, 0.5"
`correct_choices` and `correct_inputs` are used for grading the problem
and `answer_values` is used for displaying correct answers.
"""
context
=
self
.
context
self
.
correct_choices
=
{}
self
.
assign_choice_names
()
self
.
correct_inputs
=
{}
self
.
answer_values
=
{
self
.
answer_id
:
[]}
correct_xml
=
self
.
xml
.
xpath
(
'//*[@id=$id]//choice[@correct="true"]'
,
id
=
self
.
xml
.
get
(
'id'
))
for
node
in
correct_xml
:
# For each correct choice, set the `parent_name` to the
# current choice's name
parent_name
=
node
.
get
(
'name'
)
# Add the name of the correct binary choice to the
# correct choices list as a key. The value is not important.
self
.
correct_choices
[
parent_name
]
=
{
'answer'
:
''
}
# Add the name of the parent to the list of correct answers
self
.
answer_values
[
self
.
answer_id
]
.
append
(
parent_name
)
answer_list
=
[]
# Loop over <numtolerance_input> elements inside of the correct choices
for
child
in
node
:
answer
=
child
.
get
(
'answer'
,
None
)
if
not
answer
:
# If the question creator does not specify an answer for a
# <numtolerance_input> inside of a correct choice, raise an error
raise
LoncapaProblemError
(
"Answer not provided for numtolerance_input"
)
# Contextualize the answer to allow script generated answers.
answer
=
contextualize_text
(
answer
,
context
)
input_name
=
child
.
get
(
'name'
)
# Contextualize the tolerance to value.
tolerance
=
contextualize_text
(
child
.
get
(
'tolerance'
,
'0'
),
context
)
# Add the answer and tolerance information for the current
# numtolerance_input to `correct_inputs`
self
.
correct_inputs
[
input_name
]
=
{
'answer'
:
answer
,
'tolerance'
:
tolerance
}
# Add the correct answer for this input to the list for show
answer_list
.
append
(
answer
)
# Turn the list of numtolerance_input answers into a comma separated string.
self
.
answer_values
[
parent_name
]
=
', '
.
join
(
answer_list
)
# Turn correct choices into a set. Allows faster grading.
self
.
correct_choices
=
set
(
self
.
correct_choices
.
keys
())
def
assign_choice_names
(
self
):
"""
Initialize name attributes in <choice> and <numtolerance_input> tags
for this response.
Example:
Assuming for simplicity that `self.answer_id` = '1_2_1'
Before the function is called `self.xml` =
<radiotextgroup>
<choice correct = "true">
The number
<numtolerance_input answer="5"/>
Is the mean of the list.
</choice>
<choice correct = "false">
False demonstration choice
</choice>
</radiotextgroup>
After this is called the choices and numtolerance_inputs will have a name
attribute initialized and self.xml will be:
<radiotextgroup>
<choice correct = "true" name ="1_2_1_choiceinput_0bc">
The number
<numtolerance_input name = "1_2_1_choiceinput0_numtolerance_input_0"
answer="5"/>
Is the mean of the list.
</choice>
<choice correct = "false" name = "1_2_1_choiceinput_1bc>
False demonstration choice
</choice>
</radiotextgroup>
"""
for
index
,
choice
in
enumerate
(
self
.
xml
.
xpath
(
'//*[@id=$id]//choice'
,
id
=
self
.
xml
.
get
(
'id'
))
):
# Set the name attribute for <choices>
# "bc" is appended at the end to indicate that this is a
# binary choice as opposed to a numtolerance_input, this convention
# is used when grading the problem
choice
.
set
(
"name"
,
self
.
answer_id
+
"_choiceinput_"
+
str
(
index
)
+
"bc"
)
# Set Name attributes for <numtolerance_input> elements
# Look for all <numtolerance_inputs> inside this choice.
numtolerance_inputs
=
choice
.
findall
(
'numtolerance_input'
)
# Look for all <decoy_input> inside this choice
decoys
=
choice
.
findall
(
'decoy_input'
)
# <decoy_input> would only be used in choices which do not contain
# <numtolerance_input>
inputs
=
numtolerance_inputs
if
numtolerance_inputs
else
decoys
# Give each input inside of the choice a name combining
# The ordinality of the choice, and the ordinality of the input
# within that choice e.g. 1_2_1_choiceinput_0_numtolerance_input_1
for
ind
,
child
in
enumerate
(
inputs
):
child
.
set
(
"name"
,
self
.
answer_id
+
"_choiceinput_"
+
str
(
index
)
+
"_numtolerance_input_"
+
str
(
ind
)
)
def
get_score
(
self
,
student_answers
):
"""
Returns a `CorrectMap` showing whether `student_answers` are correct.
`student_answers` contains keys for binary inputs(radiobutton,
checkbox) and numerical inputs. Keys ending with 'bc' are binary
choice inputs otherwise they are text fields.
This method first separates the two
types of answers and then grades them in separate methods.
The student is only correct if they have both the binary inputs and
numerical inputs correct.
"""
answer_dict
=
student_answers
.
get
(
self
.
answer_id
,
""
)
binary_choices
,
numtolerance_inputs
=
self
.
_split_answers_dict
(
answer_dict
)
# Check the binary choices first.
choices_correct
=
self
.
_check_student_choices
(
binary_choices
)
inputs_correct
=
True
inputs_correct
=
self
.
_check_student_inputs
(
numtolerance_inputs
)
# Only return correct if the student got both the binary
# and numtolerance_inputs are correct
correct
=
choices_correct
and
inputs_correct
return
CorrectMap
(
self
.
answer_id
,
'correct'
if
correct
else
'incorrect'
)
def
get_answers
(
self
):
"""
Returns a dictionary containing the names of binary choices as keys
and a string of answers to any numtolerance_inputs which they may have
e.g {choice_1bc : "answer1, answer2", choice_2bc : ""}
"""
return
self
.
answer_values
def
_split_answers_dict
(
self
,
a_dict
):
"""
Returns two dicts:
`binary_choices` : dictionary {input_name: input_value} for
the binary choices which the student selected.
and
`numtolerance_choices` : a dictionary {input_name: input_value}
for the numtolerance_inputs inside of choices which were selected
Determines if an input is inside of a binary input by looking at
the beginning of it's name.
For example. If a binary_choice was named '1_2_1_choiceinput_0bc'
All of the numtolerance_inputs in it would have an idea that begins
with '1_2_1_choice_input_0_numtolerance_input'
Splits the name of the numtolerance_input at the occurence of
'_numtolerance_input_' and appends 'bc' to the end to get the name
of the choice it is contained in.
Example:
`a_dict` = {
'1_2_1_choiceinput_0bc': '1_2_1_choiceinput_0bc',
'1_2_1_choiceinput_0_numtolerance_input_0': '1',
'1_2_1_choiceinput_0_numtolerance_input_1': '2'
'1_2_1_choiceinput_1_numtolerance_input_0': '3'
}
In this case, the binary choice is '1_2_1_choiceinput_0bc', and
the numtolerance_inputs associated with it are
'1_2_1_choiceinput_0_numtolerance_input_0', and
'1_2_1_choiceinput_0_numtolerance_input_1'.
so the two return dictionaries would be
`binary_choices` = {'1_2_1_choiceinput_0bc': '1_2_1_choiceinput_0bc'}
and
`numtolerance_choices` ={
'1_2_1_choiceinput_0_numtolerance_input_0': '1',
'1_2_1_choiceinput_0_numtolerance_input_1': '2'
}
The entry '1_2_1_choiceinput_1_numtolerance_input_0': '3' is discarded
because it was not inside of a selected binary choice, and no validation
should be performed on numtolerance_inputs inside of non-selected choices.
"""
# Initialize the two dictionaries that are returned
numtolerance_choices
=
{}
binary_choices
=
{}
# `selected_choices` is a list of binary choices which were "checked/selected"
# when the student submitted the problem.
# Keys in a_dict ending with 'bc' refer to binary choices.
selected_choices
=
[
key
for
key
in
a_dict
if
key
.
endswith
(
"bc"
)]
for
key
in
selected_choices
:
binary_choices
[
key
]
=
a_dict
[
key
]
# Convert the name of a numtolerance_input into the name of the binary
# choice that it is contained within, and append it to the list if
# the numtolerance_input's parent binary_choice is contained in
# `selected_choices`.
selected_numtolerance_inputs
=
[
key
for
key
in
a_dict
if
key
.
partition
(
"_numtolerance_input_"
)[
0
]
+
"bc"
in
selected_choices
]
for
key
in
selected_numtolerance_inputs
:
numtolerance_choices
[
key
]
=
a_dict
[
key
]
return
(
binary_choices
,
numtolerance_choices
)
def
_check_student_choices
(
self
,
choices
):
"""
Compares student submitted checkbox/radiobutton answers against
the correct answers. Returns True or False.
True if all of the correct choices are selected and no incorrect
choices are selected.
"""
student_choices
=
set
(
choices
)
required_selected
=
len
(
self
.
correct_choices
-
student_choices
)
==
0
no_extra_selected
=
len
(
student_choices
-
self
.
correct_choices
)
==
0
correct
=
required_selected
and
no_extra_selected
return
correct
def
_check_student_inputs
(
self
,
numtolerance_inputs
):
"""
Compares student submitted numerical answers against the correct
answers and tolerances.
`numtolerance_inputs` is a dictionary {answer_name : answer_value}
Performs numerical validation by means of calling
`compare_with_tolerance()` on all of `numtolerance_inputs`
Performs a call to `compare_with_tolerance` even on values for
decoy_inputs. This is used to validate their numericality and
raise an error if the student entered a non numerical expression.
Returns True if and only if all student inputs are correct.
"""
inputs_correct
=
True
for
answer_name
,
answer_value
in
numtolerance_inputs
.
iteritems
():
# If `self.corrrect_inputs` does not contain an entry for
# `answer_name`, this means that answer_name is a decoy
# input's value, and validation of its numericality is the
# only thing of interest from the later call to
# `compare_with_tolerance`.
params
=
self
.
correct_inputs
.
get
(
answer_name
,
{
'answer'
:
0
})
correct_ans
=
params
[
'answer'
]
# Set the tolerance to '0' if it was not specified in the xml
tolerance
=
params
.
get
(
'tolerance'
,
'0'
)
# Make sure that the staff answer is a valid number
try
:
correct_ans
=
complex
(
correct_ans
)
except
ValueError
:
log
.
debug
(
"Content error--answer"
+
"'{0}' is not a valid complex number"
.
format
(
correct_ans
)
)
raise
StudentInputError
(
"The Staff answer could not be interpreted as a number."
)
# Compare the student answer to the staff answer/ or to 0
# if all that is important is verifying numericality
try
:
partial_correct
=
compare_with_tolerance
(
evaluator
(
dict
(),
dict
(),
answer_value
),
correct_ans
,
tolerance
)
except
:
# Use the traceback-preserving version of re-raising with a
# different type
_
,
_
,
trace
=
sys
.
exc_info
()
raise
StudentInputError
(
"Could not interpret '{0}' as a number{1}"
.
format
(
cgi
.
escape
(
answer_value
),
trace
)
)
# Ignore the results of the comparisons which were just for
# Numerical Validation.
if
answer_name
in
self
.
correct_inputs
and
not
partial_correct
:
# If any input is not correct, set the return value to False
inputs_correct
=
False
return
inputs_correct
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# TEMPORARY: List of all response subclasses
# TEMPORARY: List of all response subclasses
...
@@ -2116,4 +2445,5 @@ __all__ = [CodeResponse,
...
@@ -2116,4 +2445,5 @@ __all__ = [CodeResponse,
MultipleChoiceResponse
,
MultipleChoiceResponse
,
TrueFalseResponse
,
TrueFalseResponse
,
JavascriptResponse
,
JavascriptResponse
,
AnnotationResponse
]
AnnotationResponse
,
ChoiceTextResponse
]
common/lib/capa/capa/templates/choicetext.html
0 → 100644
View file @
46ae2f9c
<
%
element_checked =
False
%
>
% for choice_id, _ in choices:
<
%
choice_id =
choice_id
%
>
%if choice_id in value:
<
%
element_checked =
True
%
>
%endif
%endfor
<section
id=
"choicetextinput_${id}"
class=
"choicetextinput"
>
<form
class=
"choicetextgroup capa_inputtype"
id=
"inputtype_${id}"
>
<div
class=
"script_placeholder"
data-src=
"/static/js/capa/choicetextinput.js"
/>
<div
class=
"indicator_container"
>
% if input_type == 'checkbox' or not element_checked:
% if status == 'unsubmitted':
<span
class=
"unanswered"
style=
"display:inline-block;"
id=
"status_${id}"
></span>
% elif status == 'correct':
<span
class=
"correct"
id=
"status_${id}"
></span>
% elif status == 'incorrect':
<span
class=
"incorrect"
id=
"status_${id}"
></span>
% elif status == 'incomplete':
<span
class=
"incorrect"
id=
"status_${id}"
></span>
% endif
% endif
</div>
<fieldset>
% for choice_id, choice_description in choices:
<
%
choice_id=
choice_id
%
>
<section
id=
"forinput${choice_id}"
%
if
input_type =
=
'
radio
'
and
choice_id
in
value
:
<%
if
status =
=
'
correct
'
:
correctness =
'correct'
elif
status =
=
'
incorrect
'
:
correctness =
'incorrect'
else:
correctness =
None
%
>
% if correctness:
class="choicetextgroup_${correctness}"
% endif
% endif
>
<input
class=
"ctinput"
type=
"${input_type}"
name=
"choiceinput_${id}"
id=
"${choice_id}"
value=
"${choice_id}"
%
if
choice_id
in
value:
checked=
"true"
%
endif
/>
% for content_node in choice_description:
% if content_node['type'] == 'text':
<span
class=
"mock_label"
>
${content_node['contents']}
</span>
% else:
<
%
my_id =
content_node.get('contents','')
%
>
<
%
my_val =
value.get(my_id,'')
%
>
<input
class=
"ctinput"
type=
"text"
name=
"${content_node['contents']}"
id=
"${content_node['contents']}"
value=
"${my_val|h} "
/>
%endif
<span
class=
"mock_label"
>
${content_node['tail_text']}
</span>
% endfor
<p
id=
"answer_${choice_id}"
class=
"answer"
></p>
</section>
% endfor
<span
id=
"answer_${id}"
></span>
</fieldset>
<input
class=
"choicetextvalue"
type=
"hidden"
name=
"input_${id}{}"
id=
"input_${id}"
value=
"${value|h}"
/>
% if show_correctness == "never" and (value or status not in ['unsubmitted']):
<div
class=
"capa_alert"
>
${submitted_message}
</div>
%endif
</form>
</section>
common/lib/capa/capa/tests/response_xml_factory.py
View file @
46ae2f9c
...
@@ -779,3 +779,109 @@ class SymbolicResponseXMLFactory(ResponseXMLFactory):
...
@@ -779,3 +779,109 @@ class SymbolicResponseXMLFactory(ResponseXMLFactory):
def
create_input_element
(
self
,
**
kwargs
):
def
create_input_element
(
self
,
**
kwargs
):
return
ResponseXMLFactory
.
textline_input_xml
(
**
kwargs
)
return
ResponseXMLFactory
.
textline_input_xml
(
**
kwargs
)
class
ChoiceTextResponseXMLFactory
(
ResponseXMLFactory
):
""" Factory for producing <choicetextresponse> xml """
def
create_response_element
(
self
,
**
kwargs
):
""" Create a <choicetextresponse> element """
return
etree
.
Element
(
"choicetextresponse"
)
def
create_input_element
(
self
,
**
kwargs
):
""" Create a <checkboxgroup> element.
choices can be specified in the following format:
[("true", [{"answer": "5", "tolerance": 0}]),
("false", [{"answer": "5", "tolerance": 0}])
]
This indicates that the first checkbox/radio is correct and it
contains a numtolerance_input with an answer of 5 and a tolerance of 0
It also indicates that the second has a second incorrect radiobutton
or checkbox with a numtolerance_input.
"""
choices
=
kwargs
.
get
(
'choices'
,
[(
"true"
,
{})])
choice_inputs
=
[]
# Ensure that the first element of choices is an ordered
# collection. It will start as a list, a tuple, or not a Container.
if
type
(
choices
[
0
])
not
in
[
list
,
tuple
]:
choices
=
[
choices
]
for
choice
in
choices
:
correctness
,
answers
=
choice
numtolerance_inputs
=
[]
# If the current `choice` contains any("answer": number)
# elements, turn those into numtolerance_inputs
if
answers
:
# `answers` will be a list or tuple of answers or a single
# answer, representing the answers for numtolerance_inputs
# inside of this specific choice.
# Make sure that `answers` is an ordered collection for
# convenience.
if
type
(
answers
)
not
in
[
list
,
tuple
]:
answers
=
[
answers
]
numtolerance_inputs
=
[
self
.
_create_numtolerance_input_element
(
answer
)
for
answer
in
answers
]
choice_inputs
.
append
(
self
.
_create_choice_element
(
correctness
=
correctness
,
inputs
=
numtolerance_inputs
)
)
# Default type is 'radiotextgroup'
input_type
=
kwargs
.
get
(
'type'
,
'radiotextgroup'
)
input_element
=
etree
.
Element
(
input_type
)
for
ind
,
choice
in
enumerate
(
choice_inputs
):
# Give each choice text equal to it's position(0,1,2...)
choice
.
text
=
"choice_{0}"
.
format
(
ind
)
input_element
.
append
(
choice
)
return
input_element
def
_create_choice_element
(
self
,
**
kwargs
):
"""
Creates a choice element for a choictextproblem.
Defaults to a correct choice with no numtolerance_input
"""
text
=
kwargs
.
get
(
'text'
,
''
)
correct
=
kwargs
.
get
(
'correctness'
,
"true"
)
inputs
=
kwargs
.
get
(
'inputs'
,
[])
choice_element
=
etree
.
Element
(
"choice"
)
choice_element
.
set
(
"correct"
,
correct
)
choice_element
.
text
=
text
for
inp
in
inputs
:
# Add all of the inputs as children of this element
choice_element
.
append
(
inp
)
return
choice_element
def
_create_numtolerance_input_element
(
self
,
params
):
"""
Creates a <numtolerance_input/> element with optionally
specified tolerance and answer.
"""
answer
=
params
[
'answer'
]
if
'answer'
in
params
else
None
# If there is not an answer specified, Then create a <decoy_input/>
# otherwise create a <numtolerance_input/> and set its tolerance
# and answer attributes.
if
answer
:
text_input
=
etree
.
Element
(
"numtolerance_input"
)
text_input
.
set
(
'answer'
,
answer
)
# If tolerance was specified, was specified use it, otherwise
# Set the tolerance to "0"
text_input
.
set
(
'tolerance'
,
params
[
'tolerance'
]
if
'tolerance'
in
params
else
"0"
)
else
:
text_input
=
etree
.
Element
(
"decoy_input"
)
return
text_input
common/lib/capa/capa/tests/test_input_templates.py
View file @
46ae2f9c
...
@@ -714,3 +714,170 @@ class DragAndDropTemplateTest(TemplateTestCase):
...
@@ -714,3 +714,170 @@ class DragAndDropTemplateTest(TemplateTestCase):
# escaping the HTML. We should be able to traverse the XML tree.
# escaping the HTML. We should be able to traverse the XML tree.
xpath
=
"//div[@class='drag_and_drop_problem_json']/p/b"
xpath
=
"//div[@class='drag_and_drop_problem_json']/p/b"
self
.
assert_has_text
(
xml
,
xpath
,
'HTML'
)
self
.
assert_has_text
(
xml
,
xpath
,
'HTML'
)
class
ChoiceTextGroupTemplateTest
(
TemplateTestCase
):
"""Test mako template for `<choicetextgroup>` input"""
TEMPLATE_NAME
=
'choicetext.html'
VALUE_DICT
=
{
'1_choiceinput_0bc'
:
'1_choiceinput_0bc'
,
'1_choiceinput_0_textinput_0'
:
'0'
,
'1_choiceinput_1_textinput_0'
:
'0'
}
EMPTY_DICT
=
{
'1_choiceinput_0_textinput_0'
:
''
,
'1_choiceinput_1_textinput_0'
:
''
}
BOTH_CHOICE_CHECKBOX
=
{
'1_choiceinput_0bc'
:
'choiceinput_0'
,
'1_choiceinput_1bc'
:
'choiceinput_1'
,
'1_choiceinput_0_textinput_0'
:
'0'
,
'1_choiceinput_1_textinput_0'
:
'0'
}
WRONG_CHOICE_CHECKBOX
=
{
'1_choiceinput_1bc'
:
'choiceinput_1'
,
'1_choiceinput_0_textinput_0'
:
'0'
,
'1_choiceinput_1_textinput_0'
:
'0'
}
def
setUp
(
self
):
choices
=
[(
'1_choiceinput_0bc'
,
[{
'tail_text'
:
''
,
'type'
:
'text'
,
'value'
:
''
,
'contents'
:
''
},
{
'tail_text'
:
''
,
'type'
:
'textinput'
,
'value'
:
''
,
'contents'
:
'choiceinput_0_textinput_0'
}]),
(
'1_choiceinput_1bc'
,
[{
'tail_text'
:
''
,
'type'
:
'text'
,
'value'
:
''
,
'contents'
:
''
},
{
'tail_text'
:
''
,
'type'
:
'textinput'
,
'value'
:
''
,
'contents'
:
'choiceinput_1_textinput_0'
}])]
self
.
context
=
{
'id'
:
'1'
,
'choices'
:
choices
,
'status'
:
'correct'
,
'input_type'
:
'radio'
,
'value'
:
self
.
VALUE_DICT
}
super
(
ChoiceTextGroupTemplateTest
,
self
)
.
setUp
()
def
test_grouping_tag
(
self
):
"""
Tests whether we are using a section or a label to wrap choice elements.
Section is used for checkbox, so inputting text does not deselect
"""
input_tags
=
(
'radio'
,
'checkbox'
)
self
.
context
[
'status'
]
=
'correct'
xpath
=
"//section[@id='forinput1_choiceinput_0bc']"
self
.
context
[
'value'
]
=
{}
for
input_type
in
input_tags
:
self
.
context
[
'input_type'
]
=
input_type
xml
=
self
.
render_to_xml
(
self
.
context
)
self
.
assert_has_xpath
(
xml
,
xpath
,
self
.
context
)
def
test_problem_marked_correct
(
self
):
"""Test conditions under which the entire problem
(not a particular option) is marked correct"""
self
.
context
[
'status'
]
=
'correct'
self
.
context
[
'input_type'
]
=
'checkbox'
self
.
context
[
'value'
]
=
self
.
VALUE_DICT
# Should mark the entire problem correct
xml
=
self
.
render_to_xml
(
self
.
context
)
xpath
=
"//div[@class='indicator_container']/span[@class='correct']"
self
.
assert_has_xpath
(
xml
,
xpath
,
self
.
context
)
# Should NOT mark individual options
self
.
assert_no_xpath
(
xml
,
"//label[@class='choicetextgroup_incorrect']"
,
self
.
context
)
self
.
assert_no_xpath
(
xml
,
"//label[@class='choicetextgroup_correct']"
,
self
.
context
)
def
test_problem_marked_incorrect
(
self
):
"""Test all conditions under which the entire problem
(not a particular option) is marked incorrect"""
grouping_tags
=
{
'radio'
:
'label'
,
'checkbox'
:
'section'
}
conditions
=
[
{
'status'
:
'incorrect'
,
'input_type'
:
'radio'
,
'value'
:
{}},
{
'status'
:
'incorrect'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
WRONG_CHOICE_CHECKBOX
},
{
'status'
:
'incorrect'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
BOTH_CHOICE_CHECKBOX
},
{
'status'
:
'incorrect'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
VALUE_DICT
},
{
'status'
:
'incomplete'
,
'input_type'
:
'radio'
,
'value'
:
{}},
{
'status'
:
'incomplete'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
WRONG_CHOICE_CHECKBOX
},
{
'status'
:
'incomplete'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
BOTH_CHOICE_CHECKBOX
},
{
'status'
:
'incomplete'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
VALUE_DICT
}]
for
test_conditions
in
conditions
:
self
.
context
.
update
(
test_conditions
)
xml
=
self
.
render_to_xml
(
self
.
context
)
xpath
=
"//div[@class='indicator_container']/span[@class='incorrect']"
self
.
assert_has_xpath
(
xml
,
xpath
,
self
.
context
)
# Should NOT mark individual options
grouping_tag
=
grouping_tags
[
test_conditions
[
'input_type'
]]
self
.
assert_no_xpath
(
xml
,
"//{0}[@class='choicetextgroup_incorrect']"
.
format
(
grouping_tag
),
self
.
context
)
self
.
assert_no_xpath
(
xml
,
"//{0}[@class='choicetextgroup_correct']"
.
format
(
grouping_tag
),
self
.
context
)
def
test_problem_marked_unsubmitted
(
self
):
"""Test all conditions under which the entire problem
(not a particular option) is marked unanswered"""
grouping_tags
=
{
'radio'
:
'label'
,
'checkbox'
:
'section'
}
conditions
=
[
{
'status'
:
'unsubmitted'
,
'input_type'
:
'radio'
,
'value'
:
{}},
{
'status'
:
'unsubmitted'
,
'input_type'
:
'radio'
,
'value'
:
self
.
EMPTY_DICT
},
{
'status'
:
'unsubmitted'
,
'input_type'
:
'checkbox'
,
'value'
:
{}},
{
'status'
:
'unsubmitted'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
EMPTY_DICT
},
{
'status'
:
'unsubmitted'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
VALUE_DICT
},
{
'status'
:
'unsubmitted'
,
'input_type'
:
'checkbox'
,
'value'
:
self
.
BOTH_CHOICE_CHECKBOX
}]
self
.
context
[
'status'
]
=
'unanswered'
for
test_conditions
in
conditions
:
self
.
context
.
update
(
test_conditions
)
xml
=
self
.
render_to_xml
(
self
.
context
)
xpath
=
"//div[@class='indicator_container']/span[@class='unanswered']"
self
.
assert_has_xpath
(
xml
,
xpath
,
self
.
context
)
# Should NOT mark individual options
grouping_tag
=
grouping_tags
[
test_conditions
[
'input_type'
]]
self
.
assert_no_xpath
(
xml
,
"//{0}[@class='choicetextgroup_incorrect']"
.
format
(
grouping_tag
),
self
.
context
)
self
.
assert_no_xpath
(
xml
,
"//{0}[@class='choicetextgroup_correct']"
.
format
(
grouping_tag
),
self
.
context
)
def
test_option_marked_correct
(
self
):
"""Test conditions under which a particular option
(not the entire problem) is marked correct."""
conditions
=
[
{
'input_type'
:
'radio'
,
'value'
:
self
.
VALUE_DICT
}]
self
.
context
[
'status'
]
=
'correct'
for
test_conditions
in
conditions
:
self
.
context
.
update
(
test_conditions
)
xml
=
self
.
render_to_xml
(
self
.
context
)
xpath
=
"//section[@id='forinput1_choiceinput_0bc' and
\
@class='choicetextgroup_correct']"
self
.
assert_has_xpath
(
xml
,
xpath
,
self
.
context
)
# Should NOT mark the whole problem
xpath
=
"//div[@class='indicator_container']/span"
self
.
assert_no_xpath
(
xml
,
xpath
,
self
.
context
)
def
test_option_marked_incorrect
(
self
):
"""Test conditions under which a particular option
(not the entire problem) is marked incorrect."""
conditions
=
[
{
'input_type'
:
'radio'
,
'value'
:
self
.
VALUE_DICT
}]
self
.
context
[
'status'
]
=
'incorrect'
for
test_conditions
in
conditions
:
self
.
context
.
update
(
test_conditions
)
xml
=
self
.
render_to_xml
(
self
.
context
)
xpath
=
"//section[@id='forinput1_choiceinput_0bc' and
\
@class='choicetextgroup_incorrect']"
self
.
assert_has_xpath
(
xml
,
xpath
,
self
.
context
)
# Should NOT mark the whole problem
xpath
=
"//div[@class='indicator_container']/span"
self
.
assert_no_xpath
(
xml
,
xpath
,
self
.
context
)
common/lib/capa/capa/tests/test_inputtypes.py
View file @
46ae2f9c
...
@@ -860,3 +860,94 @@ class AnnotationInputTest(unittest.TestCase):
...
@@ -860,3 +860,94 @@ class AnnotationInputTest(unittest.TestCase):
self
.
maxDiff
=
None
self
.
maxDiff
=
None
self
.
assertDictEqual
(
context
,
expected
)
self
.
assertDictEqual
(
context
,
expected
)
class
TestChoiceText
(
unittest
.
TestCase
):
"""
Tests for checkboxtextgroup inputs
"""
@staticmethod
def
build_choice_element
(
node_type
,
contents
,
tail_text
,
value
):
"""
Builds a content node for a choice.
"""
# When xml is being parsed numtolerance_input and decoy_input tags map to textinput type
# in order to provide the template with correct rendering information.
if
node_type
in
(
'numtolerance_input'
,
'decoy_input'
):
node_type
=
'textinput'
choice
=
{
'type'
:
node_type
,
'contents'
:
contents
,
'tail_text'
:
tail_text
,
'value'
:
value
}
return
choice
def
check_group
(
self
,
tag
,
choice_tag
,
expected_input_type
):
"""
Build a radio or checkbox group, parse it and check the resuls against the
expected output.
`tag` should be 'checkboxtextgroup' or 'radiotextgroup'
`choice_tag` is either 'choice' for proper xml, or any other value to trigger an error.
`expected_input_type` is either 'radio' or 'checkbox'.
"""
xml_str
=
"""
<{tag}>
<{choice_tag} correct="false" name="choiceinput_0">this is<numtolerance_input name="choiceinput_0_textinput_0"/>false</{choice_tag}>
<choice correct="true" name="choiceinput_1">Is a number<decoy_input name="choiceinput_1_textinput_0"/><text>!</text></choice>
</{tag}>
"""
.
format
(
tag
=
tag
,
choice_tag
=
choice_tag
)
element
=
etree
.
fromstring
(
xml_str
)
state
=
{
'value'
:
'{}'
,
'id'
:
'choicetext_input'
,
'status'
:
'answered'
}
first_input
=
self
.
build_choice_element
(
'numtolerance_input'
,
'choiceinput_0_textinput_0'
,
'false'
,
''
)
second_input
=
self
.
build_choice_element
(
'decoy_input'
,
'choiceinput_1_textinput_0'
,
''
,
''
)
first_choice_content
=
self
.
build_choice_element
(
'text'
,
'this is'
,
''
,
''
)
second_choice_content
=
self
.
build_choice_element
(
'text'
,
'Is a number'
,
''
,
''
)
second_choice_text
=
self
.
build_choice_element
(
'text'
,
"!"
,
''
,
''
)
choices
=
[
(
'choiceinput_0'
,
[
first_choice_content
,
first_input
]),
(
'choiceinput_1'
,
[
second_choice_content
,
second_input
,
second_choice_text
])
]
expected
=
{
'msg'
:
''
,
'input_type'
:
expected_input_type
,
'choices'
:
choices
,
'show_correctness'
:
'always'
,
'submitted_message'
:
'Answer received.'
}
expected
.
update
(
state
)
the_input
=
lookup_tag
(
tag
)(
test_system
(),
element
,
state
)
context
=
the_input
.
_get_render_context
()
self
.
assertEqual
(
context
,
expected
)
def
test_radiotextgroup
(
self
):
"""
Test that a properly formatted radiotextgroup problem generates
expected ouputs
"""
self
.
check_group
(
'radiotextgroup'
,
'choice'
,
'radio'
)
def
test_checkboxtextgroup
(
self
):
"""
Test that a properly formatted checkboxtextgroup problem generates
expected ouput
"""
self
.
check_group
(
'checkboxtextgroup'
,
'choice'
,
'checkbox'
)
def
test_invalid_tag
(
self
):
"""
Test to ensure that an unrecognized inputtype tag causes an error
"""
with
self
.
assertRaises
(
Exception
):
self
.
check_group
(
'invalid'
,
'choice'
,
'checkbox'
)
def
test_invalid_input_tag
(
self
):
"""
Test to ensure having a tag other than <choice> inside of
a checkbox or radiotextgroup problem raises an error.
"""
with
self
.
assertRaisesRegexp
(
Exception
,
"Error in xml"
):
self
.
check_group
(
'checkboxtextgroup'
,
'invalid'
,
'checkbox'
)
common/lib/capa/capa/tests/test_responsetypes.py
View file @
46ae2f9c
...
@@ -1429,3 +1429,286 @@ class AnnotationResponseTest(ResponseTest):
...
@@ -1429,3 +1429,286 @@ class AnnotationResponseTest(ResponseTest):
msg
=
"
%
s should be marked
%
s"
%
(
answer_id
,
expected_correctness
))
msg
=
"
%
s should be marked
%
s"
%
(
answer_id
,
expected_correctness
))
self
.
assertEqual
(
expected_points
,
actual_points
,
self
.
assertEqual
(
expected_points
,
actual_points
,
msg
=
"
%
s should have
%
d points"
%
(
answer_id
,
expected_points
))
msg
=
"
%
s should have
%
d points"
%
(
answer_id
,
expected_points
))
class
ChoiceTextResponseTest
(
ResponseTest
):
from
response_xml_factory
import
ChoiceTextResponseXMLFactory
xml_factory_class
=
ChoiceTextResponseXMLFactory
one_choice_one_input
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
(
"true"
,
{
"answer"
:
"123"
,
"tolerance"
:
"1"
}),
itype
)
one_choice_two_inputs
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
[(
"true"
,
({
"answer"
:
"123"
,
"tolerance"
:
"1"
},
{
"answer"
:
"456"
,
"tolerance"
:
"10"
}))
],
itype
)
one_input_script
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
(
"true"
,
{
"answer"
:
"$computed_response"
,
"tolerance"
:
"1"
}),
itype
,
"computed_response = math.sqrt(4)"
)
one_choice_no_input
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
(
"true"
,
{}),
itype
)
two_choices_no_inputs
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
[(
"false"
,
{}),
(
"true"
,
{})],
itype
)
two_choices_one_input_1
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
[(
"false"
,
{}),
(
"true"
,
{
"answer"
:
"123"
,
"tolerance"
:
"0"
})],
itype
)
two_choices_one_input_2
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
[(
"true"
,
{}),
(
"false"
,
{
"answer"
:
"123"
,
"tolerance"
:
"0"
})],
itype
)
two_choices_two_inputs
=
lambda
itype
,
inst
:
inst
.
_make_problem
(
[(
"true"
,
{
"answer"
:
"123"
,
"tolerance"
:
"0"
}),
(
"false"
,
{
"answer"
:
"999"
,
"tolerance"
:
"0"
})],
itype
)
TEST_INPUTS
=
{
"1_choice_0_input_correct"
:
[(
True
,
[])],
"1_choice_0_input_incorrect"
:
[(
False
,
[])],
"1_choice_0_input_invalid_choice"
:
[(
False
,
[]),
(
True
,
[])],
"1_choice_1_input_correct"
:
[(
True
,
[
"123"
])],
"1_input_script_correct"
:
[(
True
,
[
"2"
])],
"1_input_script_incorrect"
:
[(
True
,
[
"3.25"
])],
"1_choice_2_inputs_correct"
:
[(
True
,
[
"123"
,
"456"
])],
"1_choice_2_inputs_tolerance"
:
[(
True
,
[
"123 + .5"
,
"456 + 9"
])],
"1_choice_2_inputs_1_wrong"
:
[(
True
,
[
"0"
,
"456"
])],
"1_choice_2_inputs_both_wrong"
:
[(
True
,
[
"0"
,
"0"
])],
"1_choice_2_inputs_inputs_blank"
:
[(
True
,
[
""
,
""
])],
"1_choice_2_inputs_empty"
:
[(
False
,
[])],
"1_choice_2_inputs_fail_tolerance"
:
[(
True
,
[
"123 + 1.5"
,
"456 + 9"
])],
"1_choice_1_input_within_tolerance"
:
[(
True
,
[
"122.5"
])],
"1_choice_1_input_answer_incorrect"
:
[(
True
,
[
"345"
])],
"1_choice_1_input_choice_incorrect"
:
[(
False
,
[
"123"
])],
"2_choices_0_inputs_correct"
:
[(
False
,
[]),
(
True
,
[])],
"2_choices_0_inputs_incorrect"
:
[(
True
,
[]),
(
False
,
[])],
"2_choices_0_inputs_blank"
:
[(
False
,
[]),
(
False
,
[])],
"2_choices_1_input_1_correct"
:
[(
False
,
[]),
(
True
,
[
"123"
])],
"2_choices_1_input_1_incorrect"
:
[(
True
,
[]),
(
False
,
[
"123"
])],
"2_choices_1_input_input_wrong"
:
[(
False
,
[]),
(
True
,
[
"321"
])],
"2_choices_1_input_1_blank"
:
[(
False
,
[]),
(
False
,
[])],
"2_choices_1_input_2_correct"
:
[(
True
,
[]),
(
False
,
[
"123"
])],
"2_choices_1_input_2_incorrect"
:
[(
False
,
[]),
(
True
,
[
"123"
])],
"2_choices_2_inputs_correct"
:
[(
True
,
[
"123"
]),
(
False
,
[])],
"2_choices_2_inputs_wrong_choice"
:
[(
False
,
[
"123"
]),
(
True
,
[])],
"2_choices_2_inputs_wrong_input"
:
[(
True
,
[
"321"
]),
(
False
,
[])]
}
TEST_SCENARIOS
=
{
"1_choice_0_input_correct"
:
(
"1_choice_0_input"
,
"correct"
),
"1_choice_0_input_incorrect"
:
(
"1_choice_0_input"
,
"incorrect"
),
"1_choice_0_input_invalid_choice"
:
(
"1_choice_0_input"
,
"incorrect"
),
"1_input_script_correct"
:
(
"1_input_script"
,
"correct"
),
"1_input_script_incorrect"
:
(
"1_input_script"
,
"incorrect"
),
"1_choice_2_inputs_correct"
:
(
"1_choice_2_inputs"
,
"correct"
),
"1_choice_2_inputs_tolerance"
:
(
"1_choice_2_inputs"
,
"correct"
),
"1_choice_2_inputs_1_wrong"
:
(
"1_choice_2_inputs"
,
"incorrect"
),
"1_choice_2_inputs_both_wrong"
:
(
"1_choice_2_inputs"
,
"incorrect"
),
"1_choice_2_inputs_inputs_blank"
:
(
"1_choice_2_inputs"
,
"incorrect"
),
"1_choice_2_inputs_empty"
:
(
"1_choice_2_inputs"
,
"incorrect"
),
"1_choice_2_inputs_fail_tolerance"
:
(
"1_choice_2_inputs"
,
"incorrect"
),
"1_choice_1_input_correct"
:
(
"1_choice_1_input"
,
"correct"
),
"1_choice_1_input_within_tolerance"
:
(
"1_choice_1_input"
,
"correct"
),
"1_choice_1_input_answer_incorrect"
:
(
"1_choice_1_input"
,
"incorrect"
),
"1_choice_1_input_choice_incorrect"
:
(
"1_choice_1_input"
,
"incorrect"
),
"2_choices_0_inputs_correct"
:
(
"2_choices_0_inputs"
,
"correct"
),
"2_choices_0_inputs_incorrect"
:
(
"2_choices_0_inputs"
,
"incorrect"
),
"2_choices_0_inputs_blank"
:
(
"2_choices_0_inputs"
,
"incorrect"
),
"2_choices_1_input_1_correct"
:
(
"2_choices_1_input_1"
,
"correct"
),
"2_choices_1_input_1_incorrect"
:
(
"2_choices_1_input_1"
,
"incorrect"
),
"2_choices_1_input_input_wrong"
:
(
"2_choices_1_input_1"
,
"incorrect"
),
"2_choices_1_input_1_blank"
:
(
"2_choices_1_input_1"
,
"incorrect"
),
"2_choices_1_input_2_correct"
:
(
"2_choices_1_input_2"
,
"correct"
),
"2_choices_1_input_2_incorrect"
:
(
"2_choices_1_input_2"
,
"incorrect"
),
"2_choices_2_inputs_correct"
:
(
"2_choices_2_inputs"
,
"correct"
),
"2_choices_2_inputs_wrong_choice"
:
(
"2_choices_2_inputs"
,
"incorrect"
),
"2_choices_2_inputs_wrong_input"
:
(
"2_choices_2_inputs"
,
"incorrect"
)
}
TEST_PROBLEMS
=
{
"1_choice_0_input"
:
one_choice_no_input
,
"1_choice_1_input"
:
one_choice_one_input
,
"1_input_script"
:
one_input_script
,
"1_choice_2_inputs"
:
one_choice_two_inputs
,
"2_choices_0_inputs"
:
two_choices_no_inputs
,
"2_choices_1_input_1"
:
two_choices_one_input_1
,
"2_choices_1_input_2"
:
two_choices_one_input_2
,
"2_choices_2_inputs"
:
two_choices_two_inputs
}
def
_make_problem
(
self
,
choices
,
in_type
=
'radiotextgroup'
,
script
=
''
):
"""
Convenience method to fill in default values for script and
type if needed, then call self.build_problem
"""
return
self
.
build_problem
(
choices
=
choices
,
type
=
in_type
,
script
=
script
)
def
_make_answer_dict
(
self
,
choice_list
):
"""
Convenience method to make generation of answers less tedious,
pass in an iterable argument with elements of the form: [bool, [ans,]]
Will generate an answer dict for those options
"""
answer_dict
=
{}
for
index
,
choice_answers_pair
in
enumerate
(
choice_list
):
# Choice is whether this choice is correct
# Answers contains a list of answers to textinpts for the choice
choice
,
answers
=
choice_answers_pair
if
choice
:
# Radio/Checkbox inputs in choicetext problems follow
# a naming convention that gives them names ending with "bc"
choice_id
=
"1_2_1_choiceinput_{index}bc"
.
format
(
index
=
index
)
choice_value
=
"choiceinput_{index}"
.
format
(
index
=
index
)
answer_dict
[
choice_id
]
=
choice_value
# Build the names for the numtolerance_inputs and add their answers
# to `answer_dict`.
for
ind
,
answer
in
enumerate
(
answers
):
# In `answer_id` `index` represents the ordinality of the
# choice and `ind` represents the ordinality of the
# numtolerance_input inside the parent choice.
answer_id
=
"1_2_1_choiceinput_{index}_numtolerance_input_{ind}"
.
format
(
index
=
index
,
ind
=
ind
)
answer_dict
[
answer_id
]
=
answer
return
answer_dict
def
test_invalid_xml
(
self
):
with
self
.
assertRaises
(
Exception
):
self
.
build_problem
(
type
=
"invalidtextgroup"
)
def
test_valid_xml
(
self
):
self
.
build_problem
()
self
.
assertTrue
(
True
)
def
test_interpret_error
(
self
):
one_choice_one_input
=
lambda
itype
:
self
.
_make_problem
(
(
"true"
,
{
"answer"
:
"123"
,
"tolerance"
:
"1"
}),
itype
)
with
self
.
assertRaisesRegexp
(
StudentInputError
,
"Could not interpret"
):
self
.
assert_grade
(
one_choice_one_input
(
'radiotextgroup'
),
self
.
_make_answer_dict
([(
True
,
[
"Platypus"
])]),
"correct"
)
def
test_staff_answer_error
(
self
):
broken_problem
=
self
.
_make_problem
(
[(
"true"
,
{
"answer"
:
"Platypus"
,
"tolerance"
:
"0"
}),
(
"true"
,
{
"answer"
:
"edX"
,
"tolerance"
:
"0"
})
],
"checkboxtextgroup"
)
with
self
.
assertRaisesRegexp
(
StudentInputError
,
"The Staff answer could not be interpreted as a number."
):
self
.
assert_grade
(
broken_problem
,
self
.
_make_answer_dict
(
[(
True
,
[
"1"
]),
(
True
,
[
"1"
])]
),
"correct"
)
def
test_radio_grades
(
self
):
for
name
,
inputs
in
self
.
TEST_INPUTS
.
iteritems
():
submission
=
self
.
_make_answer_dict
(
inputs
)
problem_name
,
correctness
=
self
.
TEST_SCENARIOS
[
name
]
problem
=
self
.
TEST_PROBLEMS
[
problem_name
]
self
.
assert_grade
(
problem
(
'radiotextgroup'
,
self
),
submission
,
correctness
,
msg
=
"{0} should be {1}"
.
format
(
name
,
correctness
)
)
def
test_checkbox_grades
(
self
):
scenarios
=
{
"2_choices_correct"
:
(
"checkbox_two_choices"
,
"correct"
),
"2_choices_incorrect"
:
(
"checkbox_two_choices"
,
"incorrect"
),
"2_choices_2_inputs_correct"
:
(
"checkbox_2_choices_2_inputs"
,
"correct"
),
"2_choices_2_inputs_missing_choice"
:
(
"checkbox_2_choices_2_inputs"
,
"incorrect"
),
"2_choices_2_inputs_wrong_input"
:
(
"checkbox_2_choices_2_inputs"
,
"incorrect"
)
}
inputs
=
{
"2_choices_correct"
:
[(
True
,
[]),
(
True
,
[])],
"2_choices_incorrect"
:
[(
True
,
[]),
(
False
,
[])],
"2_choices_2_inputs_correct"
:
[(
True
,
[
"123"
]),
(
True
,
[
"456"
])],
"2_choices_2_inputs_missing_choice"
:
[
(
True
,
[
"123"
]),
(
False
,
[
"456"
])
],
"2_choices_2_inputs_wrong_input"
:
[
(
True
,
[
"123"
]),
(
True
,
[
"654"
])
]
}
checkbox_two_choices
=
self
.
_make_problem
(
[(
"true"
,
{}),
(
"true"
,
{})],
"checkboxtextgroup"
)
checkbox_two_choices_two_inputs
=
self
.
_make_problem
(
[(
"true"
,
{
"answer"
:
"123"
,
"tolerance"
:
"0"
}),
(
"true"
,
{
"answer"
:
"456"
,
"tolerance"
:
"0"
})
],
"checkboxtextgroup"
)
problems
=
{
"checkbox_two_choices"
:
checkbox_two_choices
,
"checkbox_2_choices_2_inputs"
:
checkbox_two_choices_two_inputs
}
problems
.
update
(
self
.
TEST_PROBLEMS
)
for
name
,
inputs
in
inputs
.
iteritems
():
submission
=
self
.
_make_answer_dict
(
inputs
)
problem_name
,
correctness
=
scenarios
[
name
]
problem
=
problems
[
problem_name
]
self
.
assert_grade
(
problem
,
submission
,
correctness
,
msg
=
"{0} should be {1}"
.
format
(
name
,
correctness
)
)
common/lib/xmodule/xmodule/capa_module.py
View file @
46ae2f9c
...
@@ -776,6 +776,13 @@ class CapaModule(CapaFields, XModule):
...
@@ -776,6 +776,13 @@ class CapaModule(CapaFields, XModule):
then the output dict would contain {'1': ['test'] }
then the output dict would contain {'1': ['test'] }
(the value is a list).
(the value is a list).
Some other inputs such as ChoiceTextInput expect a dict of values in the returned
dict If the key ends with '{}' then we will assume that the value is a json
encoded dict and deserialize it.
For example, if the `data` dict contains {'input_1{}': '{"1_2_1": 1}'}
then the output dict would contain {'1': {"1_2_1": 1} }
(the value is a dictionary)
Raises an exception if:
Raises an exception if:
-A key in the `data` dictionary does not contain at least one underscore
-A key in the `data` dictionary does not contain at least one underscore
...
@@ -802,11 +809,21 @@ class CapaModule(CapaFields, XModule):
...
@@ -802,11 +809,21 @@ class CapaModule(CapaFields, XModule):
# the same form input (e.g. checkbox inputs). The convention is that
# the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the
# if the name ends with '[]' (which looks like an array), then the
# answer will be an array.
# answer will be an array.
# if the name ends with '{}' (Which looks like a dict),
# then the answer will be a dict
is_list_key
=
name
.
endswith
(
'[]'
)
is_list_key
=
name
.
endswith
(
'[]'
)
name
=
name
[:
-
2
]
if
is_list_key
else
name
is_dict_key
=
name
.
endswith
(
'{}'
)
name
=
name
[:
-
2
]
if
is_list_key
or
is_dict_key
else
name
if
is_list_key
:
if
is_list_key
:
val
=
data
.
getlist
(
key
)
val
=
data
.
getlist
(
key
)
elif
is_dict_key
:
try
:
val
=
json
.
loads
(
data
[
key
])
except
(
KeyError
,
ValueError
):
# Send this information along to be reported by
# The grading method
val
=
{
"error"
:
"error"
}
else
:
else
:
val
=
data
[
key
]
val
=
data
[
key
]
...
...
common/lib/xmodule/xmodule/css/capa/display.scss
View file @
46ae2f9c
...
@@ -929,4 +929,32 @@ section.problem {
...
@@ -929,4 +929,32 @@ section.problem {
}
}
}
}
}
}
.choicetextgroup
{
input
[
type
=
"text"
]
{
margin-bottom
:
0
.5em
;
}
@extend
.choicegroup
;
label
.choicetextgroup_correct
,
section
.choicetextgroup_correct
{
@extend
label
.choicegroup_correct
;
input
[
type
=
"text"
]
{
border-color
:
green
;
}
}
label
.choicetextgroup_incorrect
,
section
.choicetextgroup_incorrect
{
@extend
label
.choicegroup_incorrect
;
}
label
.choicetextgroup_show_correct
,
section
.choicetextgroup_show_correct
{
&
:after
{
content
:
url('../images/correct-icon.png')
;
margin-left
:
15px
;
}
}
span
.mock_label
{
cursor
:
default
;
}
}
}
}
common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee
View file @
46ae2f9c
...
@@ -223,6 +223,58 @@ describe 'Problem', ->
...
@@ -223,6 +223,58 @@ describe 'Problem', ->
expect
(
$
(
'label[for="input_1_1_3"]'
)).
toHaveAttr
'correct_answer'
,
'true'
expect
(
$
(
'label[for="input_1_1_3"]'
)).
toHaveAttr
'correct_answer'
,
'true'
expect
(
$
(
'label[for="input_1_2_1"]'
)).
not
.
toHaveAttr
'correct_answer'
,
'true'
expect
(
$
(
'label[for="input_1_2_1"]'
)).
not
.
toHaveAttr
'correct_answer'
,
'true'
describe
'radio text question'
,
->
radio_text_xml
=
'''
<section class="problem">
<div><p></p><span><section id="choicetextinput_1_2_1" class="choicetextinput">
<form class="choicetextgroup capa_inputtype" id="inputtype_1_2_1">
<div class="indicator_container">
<span class="unanswered" style="display:inline-block;" id="status_1_2_1"></span>
</div>
<fieldset>
<section id="forinput1_2_1_choiceinput_0bc">
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_0bc" value="choiceinput_0"">
<input class="ctinput" type="text" name="choiceinput_0_textinput_0" id="1_2_1_choiceinput_0_textinput_0" value=" ">
<p id="answer_1_2_1_choiceinput_0bc" class="answer"></p>
</>
<section id="forinput1_2_1_choiceinput_1bc">
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_1bc" value="choiceinput_1" >
<input class="ctinput" type="text" name="choiceinput_1_textinput_0" id="1_2_1_choiceinput_1_textinput_0" value=" " >
<p id="answer_1_2_1_choiceinput_1bc" class="answer"></p>
</section>
<section id="forinput1_2_1_choiceinput_2bc">
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_2bc" value="choiceinput_2" >
<input class="ctinput" type="text" name="choiceinput_2_textinput_0" id="1_2_1_choiceinput_2_textinput_0" value=" " >
<p id="answer_1_2_1_choiceinput_2bc" class="answer"></p>
</section></fieldset><input class="choicetextvalue" type="hidden" name="input_1_2_1" id="input_1_2_1"></form>
</section></span></div>
</section>
'''
beforeEach
->
# Append a radiotextresponse problem to the problem, so we can check it's javascript functionality
@
problem
.
el
.
prepend
(
radio_text_xml
)
it
'sets the correct class on the section for the correct choice'
,
->
spyOn
(
$
,
'postWithPrefix'
).
andCallFake
(
url
,
callback
)
->
callback
answers
:
"1_2_1"
:
[
"1_2_1_choiceinput_0bc"
],
"1_2_1_choiceinput_0bc"
:
"3"
@
problem
.
show
()
expect
(
$
(
'#forinput1_2_1_choiceinput_0bc'
).
attr
(
'class'
)).
toEqual
(
'choicetextgroup_show_correct'
)
expect
(
$
(
'#answer_1_2_1_choiceinput_0bc'
).
text
()).
toEqual
(
'3'
)
expect
(
$
(
'#answer_1_2_1_choiceinput_1bc'
).
text
()).
toEqual
(
''
)
expect
(
$
(
'#answer_1_2_1_choiceinput_2bc'
).
text
()).
toEqual
(
''
)
it
'Should not disable input fields'
,
->
spyOn
(
$
,
'postWithPrefix'
).
andCallFake
(
url
,
callback
)
->
callback
answers
:
"1_2_1"
:
[
"1_2_1_choiceinput_0bc"
],
"1_2_1_choiceinput_0bc"
:
"3"
@
problem
.
show
()
expect
(
$
(
'input#1_2_1_choiceinput_0bc'
).
attr
(
'disabled'
)).
not
.
toEqual
(
'disabled'
)
expect
(
$
(
'input#1_2_1_choiceinput_1bc'
).
attr
(
'disabled'
)).
not
.
toEqual
(
'disabled'
)
expect
(
$
(
'input#1_2_1_choiceinput_2bc'
).
attr
(
'disabled'
)).
not
.
toEqual
(
'disabled'
)
expect
(
$
(
'input#1_2_1'
).
attr
(
'disabled'
)).
not
.
toEqual
(
'disabled'
)
describe
'when the answers are already shown'
,
->
describe
'when the answers are already shown'
,
->
beforeEach
->
beforeEach
->
@
problem
.
el
.
addClass
'showed'
@
problem
.
el
.
addClass
'showed'
...
...
common/lib/xmodule/xmodule/js/src/capa/display.coffee
View file @
46ae2f9c
...
@@ -403,6 +403,14 @@ class @Problem
...
@@ -403,6 +403,14 @@ class @Problem
answer
=
JSON
.
parse
(
answers
[
answer_id
])
answer
=
JSON
.
parse
(
answers
[
answer_id
])
display
.
showAnswer
(
answer
)
display
.
showAnswer
(
answer
)
choicetextgroup
:
(
element
,
display
,
answers
)
=>
element
=
$
(
element
)
input_id
=
element
.
attr
(
'id'
).
replace
(
/inputtype_/
,
''
)
answer
=
answers
[
input_id
]
for
choice
in
answer
element
.
find
(
"section#forinput
#{
choice
}
"
).
addClass
'choicetextgroup_show_correct'
inputtypeHideAnswerMethods
:
inputtypeHideAnswerMethods
:
choicegroup
:
(
element
,
display
)
=>
choicegroup
:
(
element
,
display
)
=>
element
=
$
(
element
)
element
=
$
(
element
)
...
@@ -410,3 +418,7 @@ class @Problem
...
@@ -410,3 +418,7 @@ class @Problem
javascriptinput
:
(
element
,
display
)
=>
javascriptinput
:
(
element
,
display
)
=>
display
.
hideAnswer
()
display
.
hideAnswer
()
choicetextgroup
:
(
element
,
display
)
=>
element
=
$
(
element
)
element
.
find
(
"section[id^='forinput']"
).
removeClass
(
'choicetextgroup_show_correct'
)
common/static/js/capa/choicetextinput.js
0 → 100644
View file @
46ae2f9c
(
function
()
{
var
update
=
function
()
{
// Whenever a value changes create a new serialized version of this
// problem's inputs and set the hidden input fields value to equal it.
var
parent
=
$
(
this
).
closest
(
'.problems-wrapper'
);
// find the closest parent problems-wrapper and use that as the problem
// grab the input id from the input
// real_input is the hidden input field
var
real_input
=
$
(
'input.choicetextvalue'
,
parent
);
var
all_inputs
=
$
(
'.choicetextinput .ctinput'
,
parent
);
var
user_inputs
=
{};
$
(
all_inputs
).
each
(
function
(
index
,
elt
)
{
var
node
=
$
(
elt
);
var
name
=
node
.
attr
(
'id'
);
var
val
=
node
.
val
();
var
radio_value
=
node
.
attr
(
'value'
);
var
type
=
node
.
attr
(
'type'
);
var
is_checked
=
node
.
attr
(
'checked'
);
if
(
type
===
"radio"
||
type
===
"checkbox"
)
{
if
(
is_checked
===
"checked"
||
is_checked
===
"true"
)
{
user_inputs
[
name
]
=
radio_value
;
}
}
else
{
user_inputs
[
name
]
=
val
;
}
});
var
val_string
=
JSON
.
stringify
(
user_inputs
);
//this is what gets submitted as the answer, we deserialize it later
real_input
.
val
(
val_string
);
};
var
check_parent
=
function
(
event
)
{
// This looks for the containing choice of a textinput
// and sets it to be checked.
var
elt
=
$
(
event
.
target
);
var
parent_container
=
elt
.
closest
(
'section[id^="forinput"]'
);
var
choice
=
parent_container
.
find
(
"input[type='checkbox'], input[type='radio']"
);
choice
.
attr
(
"checked"
,
"checked"
);
choice
.
change
();
//need to check it then trigger the change event
};
var
imitate_label
=
function
(
event
)
{
// This causes a section to check and uncheck
// a radiobutton/checkbox whenever a user clicks on it
// If the button/checkbox is disabled, nothing happens
var
elt
=
$
(
event
.
target
);
var
parent_container
=
elt
.
closest
(
'section[id^="forinput"]'
);
var
choice
=
parent_container
.
find
(
"input[type='checkbox'], input[type='radio']"
);
if
(
choice
.
attr
(
"type"
)
===
"radio"
)
{
choice
.
attr
(
"checked"
,
"checked"
);
}
else
{
if
(
choice
.
attr
(
'checked'
))
{
choice
.
prop
(
"checked"
,
false
);
}
else
{
choice
.
prop
(
"checked"
,
true
);
}
}
choice
.
change
();
update
();
};
var
choices
=
$
(
'.mock_label'
);
var
inputs
=
$
(
'.choicetextinput .ctinput'
);
var
text_inputs
=
$
(
'.choicetextinput .ctinput[type="text"]'
);
// update on load
inputs
.
each
(
update
);
// and on every change
// This allows text inside of choices to behave as if they were part of
// a label for the choice's button/checkbox
choices
.
click
(
imitate_label
);
inputs
.
bind
(
"change"
,
update
);
text_inputs
.
click
(
check_parent
);
}).
call
(
this
);
lms/djangoapps/courseware/features/problems.feature
View file @
46ae2f9c
...
@@ -21,6 +21,8 @@ Feature: Answer problems
...
@@ -21,6 +21,8 @@ Feature: Answer problems
|
formula
|
|
formula
|
|
script
|
|
script
|
|
code
|
|
code
|
|
radio_text
|
|
checkbox_text
|
Scenario
:
I
can answer a problem incorrectly
Scenario
:
I
can answer a problem incorrectly
Given
External graders respond
"incorrect"
Given
External graders respond
"incorrect"
...
@@ -40,6 +42,8 @@ Feature: Answer problems
...
@@ -40,6 +42,8 @@ Feature: Answer problems
|
formula
|
|
formula
|
|
script
|
|
script
|
|
code
|
|
code
|
|
radio_text
|
|
checkbox_text
|
Scenario
:
I
can submit a blank answer
Scenario
:
I
can submit a blank answer
Given
I am viewing a
"<ProblemType>"
problem
Given
I am viewing a
"<ProblemType>"
problem
...
@@ -57,6 +61,8 @@ Feature: Answer problems
...
@@ -57,6 +61,8 @@ Feature: Answer problems
|
numerical
|
|
numerical
|
|
formula
|
|
formula
|
|
script
|
|
script
|
|
radio_text
|
|
checkbox_text
|
Scenario
:
I
can reset a problem
Scenario
:
I
can reset a problem
...
@@ -84,6 +90,10 @@ Feature: Answer problems
...
@@ -84,6 +90,10 @@ Feature: Answer problems
|
formula
|
incorrect
|
|
formula
|
incorrect
|
|
script
|
correct
|
|
script
|
correct
|
|
script
|
incorrect
|
|
script
|
incorrect
|
|
radio_text
|
correct
|
|
radio_text
|
incorrect
|
|
checkbox_text
|
correct
|
|
checkbox_text
|
incorrect
|
Scenario
:
I
can answer a problem with one attempt correctly and not reset
Scenario
:
I
can answer a problem with one attempt correctly and not reset
...
...
lms/djangoapps/courseware/features/problems_setup.py
View file @
46ae2f9c
...
@@ -18,7 +18,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
...
@@ -18,7 +18,7 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
ChoiceResponseXMLFactory
,
MultipleChoiceResponseXMLFactory
,
\
ChoiceResponseXMLFactory
,
MultipleChoiceResponseXMLFactory
,
\
StringResponseXMLFactory
,
NumericalResponseXMLFactory
,
\
StringResponseXMLFactory
,
NumericalResponseXMLFactory
,
\
FormulaResponseXMLFactory
,
CustomResponseXMLFactory
,
\
FormulaResponseXMLFactory
,
CustomResponseXMLFactory
,
\
CodeResponseXMLFactory
CodeResponseXMLFactory
,
ChoiceTextResponseXMLFactory
from
nose.tools
import
assert_true
from
nose.tools
import
assert_true
...
@@ -131,6 +131,32 @@ PROBLEM_DICT = {
...
@@ -131,6 +131,32 @@ PROBLEM_DICT = {
'grader_payload'
:
'{"grader": "ps1/Spring2013/test_grader.py"}'
,
},
'grader_payload'
:
'{"grader": "ps1/Spring2013/test_grader.py"}'
,
},
'correct'
:
[
'span.correct'
],
'correct'
:
[
'span.correct'
],
'incorrect'
:
[
'span.incorrect'
],
'incorrect'
:
[
'span.incorrect'
],
'unanswered'
:
[
'span.unanswered'
]},
'radio_text'
:
{
'factory'
:
ChoiceTextResponseXMLFactory
(),
'kwargs'
:
{
'question_text'
:
'The correct answer is Choice 0 and input 8'
,
'type'
:
'radiotextgroup'
,
'choices'
:
[(
"true"
,
{
"answer"
:
"8"
,
"tolerance"
:
"1"
}),
(
"false"
,
{
"answer"
:
"8"
,
"tolerance"
:
"1"
})
]
},
'correct'
:
[
'section.choicetextgroup_correct'
],
'incorrect'
:
[
'span.incorrect'
,
'section.choicetextgroup_incorrect'
],
'unanswered'
:
[
'span.unanswered'
]},
'checkbox_text'
:
{
'factory'
:
ChoiceTextResponseXMLFactory
(),
'kwargs'
:
{
'question_text'
:
'The correct answer is Choice 0 and input 8'
,
'type'
:
'checkboxtextgroup'
,
'choices'
:
[(
"true"
,
{
"answer"
:
"8"
,
"tolerance"
:
"1"
}),
(
"false"
,
{
"answer"
:
"8"
,
"tolerance"
:
"1"
})
]
},
'correct'
:
[
'span.correct'
],
'incorrect'
:
[
'span.incorrect'
],
'unanswered'
:
[
'span.unanswered'
]}
'unanswered'
:
[
'span.unanswered'
]}
}
}
...
@@ -196,6 +222,19 @@ def answer_problem(problem_type, correctness):
...
@@ -196,6 +222,19 @@ def answer_problem(problem_type, correctness):
# (configured in the problem XML above)
# (configured in the problem XML above)
pass
pass
elif
problem_type
==
'radio_text'
or
problem_type
==
'checkbox_text'
:
input_value
=
"8"
if
correctness
==
'correct'
else
"5"
choice
=
"choiceinput_0bc"
if
correctness
==
'correct'
else
"choiceinput_1bc"
world
.
css_check
(
inputfield
(
problem_type
,
choice
=
choice
))
world
.
css_fill
(
inputfield
(
problem_type
,
choice
=
"choiceinput_0_numtolerance_input_0"
),
input_value
)
def
problem_has_answer
(
problem_type
,
answer_class
):
def
problem_has_answer
(
problem_type
,
answer_class
):
if
problem_type
==
"drop down"
:
if
problem_type
==
"drop down"
:
...
@@ -244,6 +283,17 @@ def problem_has_answer(problem_type, answer_class):
...
@@ -244,6 +283,17 @@ def problem_has_answer(problem_type, answer_class):
expected
=
"x^2+2*x+y"
if
answer_class
==
'correct'
else
'x^2'
expected
=
"x^2+2*x+y"
if
answer_class
==
'correct'
else
'x^2'
assert_textfield
(
'formula'
,
expected
)
assert_textfield
(
'formula'
,
expected
)
elif
problem_type
in
(
"radio_text"
,
"checkbox_text"
):
if
answer_class
==
'blank'
:
expected
=
(
''
,
''
)
assert_choicetext_values
(
problem_type
,
(),
expected
)
elif
answer_class
==
'incorrect'
:
expected
=
(
'5'
,
''
)
assert_choicetext_values
(
problem_type
,
[
"choiceinput_1bc"
],
expected
)
else
:
expected
=
(
'8'
,
''
)
assert_choicetext_values
(
problem_type
,
[
"choiceinput_0bc"
],
expected
)
else
:
else
:
# The other response types use random data,
# The other response types use random data,
# which would be difficult to check
# which would be difficult to check
...
@@ -292,6 +342,12 @@ def inputfield(problem_type, choice=None, input_num=1):
...
@@ -292,6 +342,12 @@ def inputfield(problem_type, choice=None, input_num=1):
sel
=
(
"input#input_i4x-edx-model_course-problem-
%
s_2_
%
s"
%
sel
=
(
"input#input_i4x-edx-model_course-problem-
%
s_2_
%
s"
%
(
problem_type
.
replace
(
" "
,
"_"
),
str
(
input_num
)))
(
problem_type
.
replace
(
" "
,
"_"
),
str
(
input_num
)))
# this is necessary due to naming requirement for this problem type
if
problem_type
in
(
"radio_text"
,
"checkbox_text"
):
sel
=
"input#i4x-edx-model_course-problem-{0}_2_{1}"
.
format
(
problem_type
.
replace
(
" "
,
"_"
),
str
(
input_num
)
)
if
choice
is
not
None
:
if
choice
is
not
None
:
base
=
"_choice_"
if
problem_type
==
"multiple choice"
else
"_"
base
=
"_choice_"
if
problem_type
==
"multiple choice"
else
"_"
sel
=
sel
+
base
+
str
(
choice
)
sel
=
sel
+
base
+
str
(
choice
)
...
@@ -325,3 +381,28 @@ def assert_checked(problem_type, choices):
...
@@ -325,3 +381,28 @@ def assert_checked(problem_type, choices):
def
assert_textfield
(
problem_type
,
expected_text
,
input_num
=
1
):
def
assert_textfield
(
problem_type
,
expected_text
,
input_num
=
1
):
element_value
=
world
.
css_value
(
inputfield
(
problem_type
,
input_num
=
input_num
))
element_value
=
world
.
css_value
(
inputfield
(
problem_type
,
input_num
=
input_num
))
assert
element_value
==
expected_text
assert
element_value
==
expected_text
def
assert_choicetext_values
(
problem_type
,
choices
,
expected_values
):
"""
Asserts that only the given choices are checked, and given
text fields have a desired value
"""
all_choices
=
[
'choiceinput_0bc'
,
'choiceinput_1bc'
]
all_inputs
=
[
"choiceinput_0_numtolerance_input_0"
,
"choiceinput_1_numtolerance_input_0"
]
for
this_choice
in
all_choices
:
element
=
world
.
css_find
(
inputfield
(
problem_type
,
choice
=
this_choice
))
if
this_choice
in
choices
:
assert
element
.
checked
else
:
assert
not
element
.
checked
for
(
name
,
expected
)
in
zip
(
all_inputs
,
expected_values
):
element
=
world
.
css_find
(
inputfield
(
problem_type
,
name
))
# Remove any trailing spaces that may have been added
assert
element
.
value
.
strip
()
==
expected
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