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
3d1c54fe
Commit
3d1c54fe
authored
Oct 24, 2014
by
Sarina Canelake
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5595 from Stanford-Online/caijim/resetbutton
Support and tests for adding a reset button to units
parents
98c9880d
6d19a0c8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
426 additions
and
217 deletions
+426
-217
CHANGELOG.rst
+2
-0
cms/djangoapps/contentstore/features/problem-editor.py
+2
-0
common/lib/xmodule/xmodule/capa_base.py
+66
-54
common/lib/xmodule/xmodule/capa_base_constants.py
+28
-0
common/lib/xmodule/xmodule/modulestore/inheritance.py
+13
-2
common/lib/xmodule/xmodule/tests/test_capa_module.py
+205
-137
lms/djangoapps/courseware/features/problems.feature
+98
-24
lms/djangoapps/courseware/features/problems.py
+12
-0
No files found.
CHANGELOG.rst
View file @
3d1c54fe
...
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
...
@@ -5,6 +5,8 @@ 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: Add configurable reset button to units
LMS: Support adding cohorts from the instructor dashboard. TNL-162
LMS: Support adding cohorts from the instructor dashboard. TNL-162
LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
LMS: Support adding students to a cohort via the instructor dashboard. TNL-163
...
...
cms/djangoapps/contentstore/features/problem-editor.py
View file @
3d1c54fe
...
@@ -13,6 +13,7 @@ MAXIMUM_ATTEMPTS = "Maximum Attempts"
...
@@ -13,6 +13,7 @@ MAXIMUM_ATTEMPTS = "Maximum Attempts"
PROBLEM_WEIGHT
=
"Problem Weight"
PROBLEM_WEIGHT
=
"Problem Weight"
RANDOMIZATION
=
'Randomization'
RANDOMIZATION
=
'Randomization'
SHOW_ANSWER
=
"Show Answer"
SHOW_ANSWER
=
"Show Answer"
SHOW_RESET_BUTTON
=
"Show Reset Button"
TIMER_BETWEEN_ATTEMPTS
=
"Timer Between Attempts"
TIMER_BETWEEN_ATTEMPTS
=
"Timer Between Attempts"
MATLAB_API_KEY
=
"Matlab API key"
MATLAB_API_KEY
=
"Matlab API key"
...
@@ -102,6 +103,7 @@ def i_see_advanced_settings_with_values(step):
...
@@ -102,6 +103,7 @@ def i_see_advanced_settings_with_values(step):
[
PROBLEM_WEIGHT
,
""
,
False
],
[
PROBLEM_WEIGHT
,
""
,
False
],
[
RANDOMIZATION
,
"Never"
,
False
],
[
RANDOMIZATION
,
"Never"
,
False
],
[
SHOW_ANSWER
,
"Finished"
,
False
],
[
SHOW_ANSWER
,
"Finished"
,
False
],
[
SHOW_RESET_BUTTON
,
"False"
,
False
],
[
TIMER_BETWEEN_ATTEMPTS
,
"0"
,
False
],
[
TIMER_BETWEEN_ATTEMPTS
,
"0"
,
False
],
])
])
...
...
common/lib/xmodule/xmodule/capa_base.py
View file @
3d1c54fe
...
@@ -29,6 +29,8 @@ from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
...
@@ -29,6 +29,8 @@ from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
from
.fields
import
Timedelta
,
Date
from
.fields
import
Timedelta
,
Date
from
django.utils.timezone
import
UTC
from
django.utils.timezone
import
UTC
from
.util.duedate
import
get_extended_due_date
from
.util.duedate
import
get_extended_due_date
from
xmodule.capa_base_constants
import
RANDOMIZATION
,
SHOWANSWER
from
django.conf
import
settings
log
=
logging
.
getLogger
(
"edx.courseware"
)
log
=
logging
.
getLogger
(
"edx.courseware"
)
...
@@ -63,9 +65,9 @@ class Randomization(String):
...
@@ -63,9 +65,9 @@ class Randomization(String):
"""
"""
def
from_json
(
self
,
value
):
def
from_json
(
self
,
value
):
if
value
in
(
""
,
"true"
):
if
value
in
(
""
,
"true"
):
return
"always"
return
RANDOMIZATION
.
ALWAYS
elif
value
==
"false"
:
elif
value
==
"false"
:
return
"per_student"
return
RANDOMIZATION
.
PER_STUDENT
return
value
return
value
to_json
=
from_json
to_json
=
from_json
...
@@ -103,15 +105,15 @@ class CapaFields(object):
...
@@ -103,15 +105,15 @@ class CapaFields(object):
max_attempts
=
Integer
(
max_attempts
=
Integer
(
display_name
=
_
(
"Maximum Attempts"
),
display_name
=
_
(
"Maximum Attempts"
),
help
=
_
(
"Defines the number of times a student can try to answer this problem. "
help
=
_
(
"Defines the number of times a student can try to answer this problem. "
"If the value is not set, infinite attempts are allowed."
),
"If the value is not set, infinite attempts are allowed."
),
values
=
{
"min"
:
0
},
scope
=
Scope
.
settings
values
=
{
"min"
:
0
},
scope
=
Scope
.
settings
)
)
due
=
Date
(
help
=
_
(
"Date that this problem is due by"
),
scope
=
Scope
.
settings
)
due
=
Date
(
help
=
_
(
"Date that this problem is due by"
),
scope
=
Scope
.
settings
)
extended_due
=
Date
(
extended_due
=
Date
(
help
=
_
(
"Date that this problem is due by for a particular student. This "
help
=
_
(
"Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due "
"can be set by an instructor, and will override the global due "
"date if it is set to a date that is later than the global due "
"date if it is set to a date that is later than the global due "
"date."
),
"date."
),
default
=
None
,
default
=
None
,
scope
=
Scope
.
user_state
,
scope
=
Scope
.
user_state
,
)
)
...
@@ -122,36 +124,45 @@ class CapaFields(object):
...
@@ -122,36 +124,45 @@ class CapaFields(object):
showanswer
=
String
(
showanswer
=
String
(
display_name
=
_
(
"Show Answer"
),
display_name
=
_
(
"Show Answer"
),
help
=
_
(
"Defines when to show the answer to the problem. "
help
=
_
(
"Defines when to show the answer to the problem. "
"A default value can be set in Advanced Settings."
),
"A default value can be set in Advanced Settings."
),
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
default
=
"finished"
,
default
=
SHOWANSWER
.
FINISHED
,
values
=
[
values
=
[
{
"display_name"
:
_
(
"Always"
),
"value"
:
"always"
},
{
"display_name"
:
_
(
"Always"
),
"value"
:
SHOWANSWER
.
ALWAYS
},
{
"display_name"
:
_
(
"Answered"
),
"value"
:
"answered"
},
{
"display_name"
:
_
(
"Answered"
),
"value"
:
SHOWANSWER
.
ANSWERED
},
{
"display_name"
:
_
(
"Attempted"
),
"value"
:
"attempted"
},
{
"display_name"
:
_
(
"Attempted"
),
"value"
:
SHOWANSWER
.
ATTEMPTED
},
{
"display_name"
:
_
(
"Closed"
),
"value"
:
"closed"
},
{
"display_name"
:
_
(
"Closed"
),
"value"
:
SHOWANSWER
.
CLOSED
},
{
"display_name"
:
_
(
"Finished"
),
"value"
:
"finished"
},
{
"display_name"
:
_
(
"Finished"
),
"value"
:
SHOWANSWER
.
FINISHED
},
{
"display_name"
:
_
(
"Correct or Past Due"
),
"value"
:
"correct_or_past_due"
},
{
"display_name"
:
_
(
"Correct or Past Due"
),
"value"
:
SHOWANSWER
.
CORRECT_OR_PAST_DUE
},
{
"display_name"
:
_
(
"Past Due"
),
"value"
:
"past_due"
},
{
"display_name"
:
_
(
"Past Due"
),
"value"
:
SHOWANSWER
.
PAST_DUE
},
{
"display_name"
:
_
(
"Never"
),
"value"
:
"never"
}]
{
"display_name"
:
_
(
"Never"
),
"value"
:
SHOWANSWER
.
NEVER
}]
)
)
force_save_button
=
Boolean
(
force_save_button
=
Boolean
(
help
=
_
(
"Whether to force the save button to appear on the page"
),
help
=
_
(
"Whether to force the save button to appear on the page"
),
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
default
=
False
default
=
False
)
)
reset_key
=
"DEFAULT_SHOW_RESET_BUTTON"
default_reset_button
=
getattr
(
settings
,
reset_key
)
if
hasattr
(
settings
,
reset_key
)
else
False
show_reset_button
=
Boolean
(
display_name
=
_
(
"Show Reset Button"
),
help
=
_
(
"Determines whether a 'Reset' button is shown so the user may reset their answer. "
"A default value can be set in Advanced Settings."
),
scope
=
Scope
.
settings
,
default
=
default_reset_button
)
rerandomize
=
Randomization
(
rerandomize
=
Randomization
(
display_name
=
_
(
"Randomization"
),
display_name
=
_
(
"Randomization"
),
help
=
_
(
"Defines how often inputs are randomized when a student loads the problem. "
help
=
_
(
"Defines how often inputs are randomized when a student loads the problem. "
"This setting only applies to problems that can have randomly generated numeric values. "
"This setting only applies to problems that can have randomly generated numeric values. "
"A default value can be set in Advanced Settings."
),
"A default value can be set in Advanced Settings."
),
default
=
"never"
,
default
=
RANDOMIZATION
.
NEVER
,
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
values
=
[
values
=
[
{
"display_name"
:
_
(
"Always"
),
"value"
:
"always"
},
{
"display_name"
:
_
(
"Always"
),
"value"
:
RANDOMIZATION
.
ALWAYS
},
{
"display_name"
:
_
(
"On Reset"
),
"value"
:
"onreset"
},
{
"display_name"
:
_
(
"On Reset"
),
"value"
:
RANDOMIZATION
.
ONRESET
},
{
"display_name"
:
_
(
"Never"
),
"value"
:
"never"
},
{
"display_name"
:
_
(
"Never"
),
"value"
:
RANDOMIZATION
.
NEVER
},
{
"display_name"
:
_
(
"Per Student"
),
"value"
:
"per_student"
}
{
"display_name"
:
_
(
"Per Student"
),
"value"
:
RANDOMIZATION
.
PER_STUDENT
}
]
]
)
)
data
=
String
(
help
=
_
(
"XML data for the problem"
),
scope
=
Scope
.
content
,
default
=
"<problem></problem>"
)
data
=
String
(
help
=
_
(
"XML data for the problem"
),
scope
=
Scope
.
content
,
default
=
"<problem></problem>"
)
...
@@ -170,7 +181,7 @@ class CapaFields(object):
...
@@ -170,7 +181,7 @@ class CapaFields(object):
weight
=
Float
(
weight
=
Float
(
display_name
=
_
(
"Problem Weight"
),
display_name
=
_
(
"Problem Weight"
),
help
=
_
(
"Defines the number of points each problem is worth. "
help
=
_
(
"Defines the number of points each problem is worth. "
"If the value is not set, each response field in the problem is worth one point."
),
"If the value is not set, each response field in the problem is worth one point."
),
values
=
{
"min"
:
0
,
"step"
:
.
1
},
values
=
{
"min"
:
0
,
"step"
:
.
1
},
scope
=
Scope
.
settings
scope
=
Scope
.
settings
)
)
...
@@ -254,7 +265,7 @@ class CapaMixin(CapaFields):
...
@@ -254,7 +265,7 @@ class CapaMixin(CapaFields):
tb
=
cgi
.
escape
(
tb
=
cgi
.
escape
(
u''
.
join
([
'Traceback (most recent call last):
\n
'
]
+
u''
.
join
([
'Traceback (most recent call last):
\n
'
]
+
traceback
.
format_tb
(
sys
.
exc_info
()[
2
])))
traceback
.
format_tb
(
sys
.
exc_info
()[
2
])))
)
)
# create a dummy problem with error message instead of failing
# create a dummy problem with error message instead of failing
problem_text
=
(
u'<problem><text><span class="inline-error">'
problem_text
=
(
u'<problem><text><span class="inline-error">'
u'Problem {url} has an error:</span>{msg}</text></problem>'
.
format
(
u'Problem {url} has an error:</span>{msg}</text></problem>'
.
format
(
...
@@ -274,9 +285,9 @@ class CapaMixin(CapaFields):
...
@@ -274,9 +285,9 @@ class CapaMixin(CapaFields):
"""
"""
Choose a new seed.
Choose a new seed.
"""
"""
if
self
.
rerandomize
==
'never'
:
if
self
.
rerandomize
==
RANDOMIZATION
.
NEVER
:
self
.
seed
=
1
self
.
seed
=
1
elif
self
.
rerandomize
==
"per_student"
and
hasattr
(
self
.
runtime
,
'seed'
):
elif
self
.
rerandomize
==
RANDOMIZATION
.
PER_STUDENT
and
hasattr
(
self
.
runtime
,
'seed'
):
# see comment on randomization_bin
# see comment on randomization_bin
self
.
seed
=
randomization_bin
(
self
.
runtime
.
seed
,
unicode
(
self
.
location
)
.
encode
(
'utf-8'
))
self
.
seed
=
randomization_bin
(
self
.
runtime
.
seed
,
unicode
(
self
.
location
)
.
encode
(
'utf-8'
))
else
:
else
:
...
@@ -446,7 +457,7 @@ class CapaMixin(CapaFields):
...
@@ -446,7 +457,7 @@ class CapaMixin(CapaFields):
"""
"""
Return True/False to indicate whether to show the "Check" button.
Return True/False to indicate whether to show the "Check" button.
"""
"""
submitted_without_reset
=
(
self
.
is_submitted
()
and
self
.
rerandomize
==
"always"
)
submitted_without_reset
=
(
self
.
is_submitted
()
and
self
.
rerandomize
==
RANDOMIZATION
.
ALWAYS
)
# If the problem is closed (past due / too many attempts)
# If the problem is closed (past due / too many attempts)
# then we do NOT show the "check" button
# then we do NOT show the "check" button
...
@@ -463,19 +474,20 @@ class CapaMixin(CapaFields):
...
@@ -463,19 +474,20 @@ class CapaMixin(CapaFields):
"""
"""
is_survey_question
=
(
self
.
max_attempts
==
0
)
is_survey_question
=
(
self
.
max_attempts
==
0
)
if
self
.
rerandomize
in
[
"always"
,
"onreset"
]:
# If the problem is closed (and not a survey question with max_attempts==0),
# then do NOT show the reset button.
if
(
self
.
closed
()
and
not
is_survey_question
):
return
False
# If the problem is closed (and not a survey question with max_attempts==0),
# Button only shows up for randomized problems if the question has been submitted
# then do NOT show the reset button.
if
self
.
rerandomize
in
[
RANDOMIZATION
.
ALWAYS
,
RANDOMIZATION
.
ONRESET
]
and
self
.
is_submitted
():
# If the problem hasn't been submitted yet, then do NOT show
return
True
# the reset button.
else
:
if
(
self
.
closed
()
and
not
is_survey_question
)
or
not
self
.
is_submitted
():
# Do NOT show the button if the problem is correct
if
self
.
is_correct
():
return
False
return
False
else
:
else
:
return
True
return
self
.
show_reset_button
# Only randomized problems need a "reset" button
else
:
return
False
def
should_show_save_button
(
self
):
def
should_show_save_button
(
self
):
"""
"""
...
@@ -489,7 +501,7 @@ class CapaMixin(CapaFields):
...
@@ -489,7 +501,7 @@ class CapaMixin(CapaFields):
return
not
self
.
closed
()
return
not
self
.
closed
()
else
:
else
:
is_survey_question
=
(
self
.
max_attempts
==
0
)
is_survey_question
=
(
self
.
max_attempts
==
0
)
needs_reset
=
self
.
is_submitted
()
and
self
.
rerandomize
==
"always"
needs_reset
=
self
.
is_submitted
()
and
self
.
rerandomize
==
RANDOMIZATION
.
ALWAYS
# If the student has unlimited attempts, and their answers
# If the student has unlimited attempts, and their answers
# are not randomized, then we do not need a save button
# are not randomized, then we do not need a save button
...
@@ -503,7 +515,7 @@ class CapaMixin(CapaFields):
...
@@ -503,7 +515,7 @@ class CapaMixin(CapaFields):
# In those cases. the if statement below is false,
# In those cases. the if statement below is false,
# and the save button can still be displayed.
# and the save button can still be displayed.
#
#
if
self
.
max_attempts
is
None
and
self
.
rerandomize
!=
"always"
:
if
self
.
max_attempts
is
None
and
self
.
rerandomize
!=
RANDOMIZATION
.
ALWAYS
:
return
False
return
False
# If the problem is closed (and not a survey question with max_attempts==0),
# If the problem is closed (and not a survey question with max_attempts==0),
...
@@ -697,28 +709,28 @@ class CapaMixin(CapaFields):
...
@@ -697,28 +709,28 @@ class CapaMixin(CapaFields):
"""
"""
if
self
.
showanswer
==
''
:
if
self
.
showanswer
==
''
:
return
False
return
False
elif
self
.
showanswer
==
"never"
:
elif
self
.
showanswer
==
SHOWANSWER
.
NEVER
:
return
False
return
False
elif
self
.
runtime
.
user_is_staff
:
elif
self
.
runtime
.
user_is_staff
:
# This is after the 'never' check because admins can see the answer
# This is after the 'never' check because admins can see the answer
# unless the problem explicitly prevents it
# unless the problem explicitly prevents it
return
True
return
True
elif
self
.
showanswer
==
'attempted'
:
elif
self
.
showanswer
==
SHOWANSWER
.
ATTEMPTED
:
return
self
.
attempts
>
0
return
self
.
attempts
>
0
elif
self
.
showanswer
==
'answered'
:
elif
self
.
showanswer
==
SHOWANSWER
.
ANSWERED
:
# NOTE: this is slightly different from 'attempted' -- resetting the problems
# NOTE: this is slightly different from 'attempted' -- resetting the problems
# makes lcp.done False, but leaves attempts unchanged.
# makes lcp.done False, but leaves attempts unchanged.
return
self
.
lcp
.
done
return
self
.
lcp
.
done
elif
self
.
showanswer
==
'closed'
:
elif
self
.
showanswer
==
SHOWANSWER
.
CLOSED
:
return
self
.
closed
()
return
self
.
closed
()
elif
self
.
showanswer
==
'finished'
:
elif
self
.
showanswer
==
SHOWANSWER
.
FINISHED
:
return
self
.
closed
()
or
self
.
is_correct
()
return
self
.
closed
()
or
self
.
is_correct
()
elif
self
.
showanswer
==
'correct_or_past_due'
:
elif
self
.
showanswer
==
SHOWANSWER
.
CORRECT_OR_PAST_DUE
:
return
self
.
is_correct
()
or
self
.
is_past_due
()
return
self
.
is_correct
()
or
self
.
is_past_due
()
elif
self
.
showanswer
==
'past_due'
:
elif
self
.
showanswer
==
SHOWANSWER
.
PAST_DUE
:
return
self
.
is_past_due
()
return
self
.
is_past_due
()
elif
self
.
showanswer
==
'always'
:
elif
self
.
showanswer
==
SHOWANSWER
.
ALWAYS
:
return
True
return
True
return
False
return
False
...
@@ -952,7 +964,7 @@ class CapaMixin(CapaFields):
...
@@ -952,7 +964,7 @@ class CapaMixin(CapaFields):
raise
NotFoundError
(
_
(
"Problem is closed."
))
raise
NotFoundError
(
_
(
"Problem is closed."
))
# Problem submitted. Student should reset before checking again
# Problem submitted. Student should reset before checking again
if
self
.
done
and
self
.
rerandomize
==
"always"
:
if
self
.
done
and
self
.
rerandomize
==
RANDOMIZATION
.
ALWAYS
:
event_info
[
'failure'
]
=
'unreset'
event_info
[
'failure'
]
=
'unreset'
self
.
track_function_unmask
(
'problem_check_fail'
,
event_info
)
self
.
track_function_unmask
(
'problem_check_fail'
,
event_info
)
if
dog_stats_api
:
if
dog_stats_api
:
...
@@ -1206,7 +1218,7 @@ class CapaMixin(CapaFields):
...
@@ -1206,7 +1218,7 @@ class CapaMixin(CapaFields):
# was presented to the user, with values interpolated etc, but that can be done
# was presented to the user, with values interpolated etc, but that can be done
# later if necessary.
# later if necessary.
variant
=
''
variant
=
''
if
self
.
rerandomize
!=
'never'
:
if
self
.
rerandomize
!=
RANDOMIZATION
.
NEVER
:
variant
=
self
.
seed
variant
=
self
.
seed
is_correct
=
correct_map
.
is_correct
(
input_id
)
is_correct
=
correct_map
.
is_correct
(
input_id
)
...
@@ -1333,7 +1345,7 @@ class CapaMixin(CapaFields):
...
@@ -1333,7 +1345,7 @@ class CapaMixin(CapaFields):
# Problem submitted. Student should reset before saving
# Problem submitted. Student should reset before saving
# again.
# again.
if
self
.
done
and
self
.
rerandomize
==
"always"
:
if
self
.
done
and
self
.
rerandomize
==
RANDOMIZATION
.
ALWAYS
:
event_info
[
'failure'
]
=
'done'
event_info
[
'failure'
]
=
'done'
self
.
track_function_unmask
(
'save_problem_fail'
,
event_info
)
self
.
track_function_unmask
(
'save_problem_fail'
,
event_info
)
return
{
return
{
...
@@ -1357,7 +1369,7 @@ class CapaMixin(CapaFields):
...
@@ -1357,7 +1369,7 @@ class CapaMixin(CapaFields):
def
reset_problem
(
self
,
_data
):
def
reset_problem
(
self
,
_data
):
"""
"""
Changes problem state to unfinished -- removes student answers,
Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself
.
Causes problem to rerender itself if randomization is enabled
.
Returns a dictionary of the form:
Returns a dictionary of the form:
{'success': True/False,
{'success': True/False,
...
@@ -1380,7 +1392,7 @@ class CapaMixin(CapaFields):
...
@@ -1380,7 +1392,7 @@ class CapaMixin(CapaFields):
'error'
:
_
(
"Problem is closed."
),
'error'
:
_
(
"Problem is closed."
),
}
}
if
not
self
.
done
:
if
not
self
.
is_submitted
()
:
event_info
[
'failure'
]
=
'not_done'
event_info
[
'failure'
]
=
'not_done'
self
.
track_function_unmask
(
'reset_problem_fail'
,
event_info
)
self
.
track_function_unmask
(
'reset_problem_fail'
,
event_info
)
return
{
return
{
...
@@ -1389,7 +1401,7 @@ class CapaMixin(CapaFields):
...
@@ -1389,7 +1401,7 @@ class CapaMixin(CapaFields):
'error'
:
_
(
"Refresh the page and make an attempt before resetting."
),
'error'
:
_
(
"Refresh the page and make an attempt before resetting."
),
}
}
if
self
.
rerandomize
in
[
"always"
,
"onreset"
]:
if
self
.
is_submitted
()
and
self
.
rerandomize
in
[
RANDOMIZATION
.
ALWAYS
,
RANDOMIZATION
.
ONRESET
]:
# Reset random number generator seed.
# Reset random number generator seed.
self
.
choose_new_seed
()
self
.
choose_new_seed
()
...
...
common/lib/xmodule/xmodule/capa_base_constants.py
0 → 100644
View file @
3d1c54fe
# -*- coding: utf-8 -*-
"""
Constants for capa_base problems
"""
class
SHOWANSWER
:
"""
Constants for when to show answer
"""
ALWAYS
=
"always"
ANSWERED
=
"answered"
ATTEMPTED
=
"attempted"
CLOSED
=
"closed"
FINISHED
=
"finished"
CORRECT_OR_PAST_DUE
=
"correct_or_past_due"
PAST_DUE
=
"past_due"
NEVER
=
"never"
class
RANDOMIZATION
:
"""
Constants for problem randomization
"""
ALWAYS
=
"always"
ONRESET
=
"onreset"
NEVER
=
"never"
PER_STUDENT
=
"per_student"
common/lib/xmodule/xmodule/modulestore/inheritance.py
View file @
3d1c54fe
"""
"""
Support for inheritance of fields down an XBlock hierarchy.
Support for inheritance of fields down an XBlock hierarchy.
"""
"""
from
__future__
import
absolute_import
from
datetime
import
datetime
from
datetime
import
datetime
from
pytz
import
UTC
from
pytz
import
UTC
from
xmodule.partitions.partitions
import
UserPartition
from
xmodule.partitions.partitions
import
UserPartition
from
xblock.fields
import
Scope
,
Boolean
,
String
,
Float
,
XBlockMixin
,
Dict
,
Integer
,
List
from
xblock.fields
import
Scope
,
Boolean
,
String
,
Float
,
XBlockMixin
,
Dict
,
Integer
,
List
from
xblock.runtime
import
KeyValueStore
,
KvsFieldData
from
xblock.runtime
import
KeyValueStore
,
KvsFieldData
from
xmodule.fields
import
Date
,
Timedelta
from
xmodule.fields
import
Date
,
Timedelta
from
django.conf
import
settings
# Make '_' a no-op so we can scrape strings
# Make '_' a no-op so we can scrape strings
_
=
lambda
text
:
text
_
=
lambda
text
:
text
...
@@ -153,6 +154,16 @@ class InheritanceMixin(XBlockMixin):
...
@@ -153,6 +154,16 @@ class InheritanceMixin(XBlockMixin):
scope
=
Scope
.
settings
scope
=
Scope
.
settings
)
)
reset_key
=
"DEFAULT_SHOW_RESET_BUTTON"
default_reset_button
=
getattr
(
settings
,
reset_key
)
if
hasattr
(
settings
,
reset_key
)
else
False
show_reset_button
=
Boolean
(
display_name
=
_
(
"Show Reset Button for Problems"
),
help
=
_
(
"Enter true or false. If true, problems default to displaying a 'Reset' button. This value may be "
"overriden in each problem's settings. Existing problems whose reset setting have not been changed are affected."
),
scope
=
Scope
.
settings
,
default
=
default_reset_button
)
def
compute_inherited_metadata
(
descriptor
):
def
compute_inherited_metadata
(
descriptor
):
"""Given a descriptor, traverse all of its descendants and do metadata
"""Given a descriptor, traverse all of its descendants and do metadata
...
...
common/lib/xmodule/xmodule/tests/test_capa_module.py
View file @
3d1c54fe
...
@@ -13,6 +13,7 @@ import random
...
@@ -13,6 +13,7 @@ import random
import
os
import
os
import
textwrap
import
textwrap
import
unittest
import
unittest
import
ddt
from
mock
import
Mock
,
patch
from
mock
import
Mock
,
patch
import
webob
import
webob
...
@@ -31,6 +32,7 @@ from xblock.fields import ScopeIds
...
@@ -31,6 +32,7 @@ from xblock.fields import ScopeIds
from
.
import
get_test_system
from
.
import
get_test_system
from
pytz
import
UTC
from
pytz
import
UTC
from
capa.correctmap
import
CorrectMap
from
capa.correctmap
import
CorrectMap
from
..capa_base_constants
import
RANDOMIZATION
class
CapaFactory
(
object
):
class
CapaFactory
(
object
):
...
@@ -140,7 +142,6 @@ class CapaFactory(object):
...
@@ -140,7 +142,6 @@ class CapaFactory(object):
return
module
return
module
class
CapaFactoryWithFiles
(
CapaFactory
):
class
CapaFactoryWithFiles
(
CapaFactory
):
"""
"""
A factory for creating a Capa problem with files attached.
A factory for creating a Capa problem with files attached.
...
@@ -182,6 +183,7 @@ if submission[0] == '':
...
@@ -182,6 +183,7 @@ if submission[0] == '':
"""
)
"""
)
@ddt.ddt
class
CapaModuleTest
(
unittest
.
TestCase
):
class
CapaModuleTest
(
unittest
.
TestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
...
@@ -540,39 +542,42 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -540,39 +542,42 @@ class CapaModuleTest(unittest.TestCase):
# Expect that number of attempts NOT incremented
# Expect that number of attempts NOT incremented
self
.
assertEqual
(
module
.
attempts
,
3
)
self
.
assertEqual
(
module
.
attempts
,
3
)
def
test_check_problem_resubmitted_with_randomize
(
self
):
@ddt.data
(
rerandomize_values
=
[
'always'
,
'true'
]
RANDOMIZATION
.
ALWAYS
,
'true'
for
rerandomize
in
rerandomize_values
:
)
# Randomize turned on
def
test_check_problem_resubmitted_with_randomize
(
self
,
rerandomize
):
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
attempts
=
0
)
# Randomize turned on
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
attempts
=
0
)
# Simulate that the problem is completed
# Simulate that the problem is completed
module
.
done
=
True
module
.
done
=
True
# Expect that we cannot submit
with
self
.
assertRaises
(
xmodule
.
exceptions
.
NotFoundError
):
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
module
.
check_problem
(
get_request_dict
)
# Expect that number of attempts NOT incremented
# Expect that we cannot submit
self
.
assertEqual
(
module
.
attempts
,
0
)
with
self
.
assertRaises
(
xmodule
.
exceptions
.
NotFoundError
):
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
module
.
check_problem
(
get_request_dict
)
def
test_check_problem_resubmitted_no_randomize
(
self
):
# Expect that number of attempts NOT incremented
rerandomize_values
=
[
'never'
,
'false'
,
'per_student'
]
self
.
assertEqual
(
module
.
attempts
,
0
)
for
rerandomize
in
rerandomize_values
:
@ddt.data
(
# Randomize turned off
RANDOMIZATION
.
NEVER
,
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
attempts
=
0
,
done
=
True
)
'false'
,
RANDOMIZATION
.
PER_STUDENT
)
def
test_check_problem_resubmitted_no_randomize
(
self
,
rerandomize
):
# Randomize turned off
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
attempts
=
0
,
done
=
True
)
# Expect that we can submit successfully
# Expect that we can submit successfully
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
result
=
module
.
check_problem
(
get_request_dict
)
result
=
module
.
check_problem
(
get_request_dict
)
self
.
assertEqual
(
result
[
'success'
],
'correct'
)
self
.
assertEqual
(
result
[
'success'
],
'correct'
)
# Expect that number of attempts IS incremented
# Expect that number of attempts IS incremented
self
.
assertEqual
(
module
.
attempts
,
1
)
self
.
assertEqual
(
module
.
attempts
,
1
)
def
test_check_problem_queued
(
self
):
def
test_check_problem_queued
(
self
):
module
=
CapaFactory
.
create
(
attempts
=
1
)
module
=
CapaFactory
.
create
(
attempts
=
1
)
...
@@ -813,7 +818,7 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -813,7 +818,7 @@ class CapaModuleTest(unittest.TestCase):
def
test_reset_problem_closed
(
self
):
def
test_reset_problem_closed
(
self
):
# pre studio default
# pre studio default
module
=
CapaFactory
.
create
(
rerandomize
=
"always"
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
)
# Simulate that the problem is closed
# Simulate that the problem is closed
with
patch
(
'xmodule.capa_module.CapaModule.closed'
)
as
mock_closed
:
with
patch
(
'xmodule.capa_module.CapaModule.closed'
)
as
mock_closed
:
...
@@ -944,35 +949,36 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -944,35 +949,36 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the result is failure
# Expect that the result is failure
self
.
assertTrue
(
'success'
in
result
and
not
result
[
'success'
])
self
.
assertTrue
(
'success'
in
result
and
not
result
[
'success'
])
def
test_save_problem_submitted_with_randomize
(
self
):
@ddt.data
(
RANDOMIZATION
.
ALWAYS
,
'true'
)
def
test_save_problem_submitted_with_randomize
(
self
,
rerandomize
):
# Capa XModule treats 'always' and 'true' equivalently
# Capa XModule treats 'always' and 'true' equivalently
rerandomize_values
=
[
'always'
,
'true'
]
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
done
=
True
)
for
rerandomize
in
rerandomize_values
:
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
done
=
True
)
# Try to save
# Try to save
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
result
=
module
.
save_problem
(
get_request_dict
)
result
=
module
.
save_problem
(
get_request_dict
)
# Expect that we cannot save
self
.
assertTrue
(
'success'
in
result
and
not
result
[
'success'
])
def
test_save_problem_submitted_no_randomize
(
self
):
# Expect that we cannot save
self
.
assertTrue
(
'success'
in
result
and
not
result
[
'success'
])
@ddt.data
(
RANDOMIZATION
.
NEVER
,
'false'
,
RANDOMIZATION
.
PER_STUDENT
)
def
test_save_problem_submitted_no_randomize
(
self
,
rerandomize
):
# Capa XModule treats 'false' and 'per_student' equivalently
# Capa XModule treats 'false' and 'per_student' equivalently
rerandomize_values
=
[
'never'
,
'false'
,
'per_student'
]
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
done
=
True
)
for
rerandomize
in
rerandomize_values
:
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
done
=
True
)
# Try to save
# Try to save
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
result
=
module
.
save_problem
(
get_request_dict
)
result
=
module
.
save_problem
(
get_request_dict
)
# Expect that we succeed
# Expect that we succeed
self
.
assertTrue
(
'success'
in
result
and
result
[
'success'
])
self
.
assertTrue
(
'success'
in
result
and
result
[
'success'
])
def
test_check_button_name
(
self
):
def
test_check_button_name
(
self
):
...
@@ -1066,7 +1072,7 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1066,7 +1072,7 @@ class CapaModuleTest(unittest.TestCase):
# If user submitted a problem but hasn't reset,
# If user submitted a problem but hasn't reset,
# do NOT show the check button
# do NOT show the check button
# Note: we can only reset when rerandomize="always" or "true"
# Note: we can only reset when rerandomize="always" or "true"
module
=
CapaFactory
.
create
(
rerandomize
=
"always"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
done
=
True
)
self
.
assertFalse
(
module
.
should_show_check_button
())
self
.
assertFalse
(
module
.
should_show_check_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
"true"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
"true"
,
done
=
True
)
...
@@ -1080,13 +1086,13 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1080,13 +1086,13 @@ class CapaModuleTest(unittest.TestCase):
# and we do NOT have a reset button, then we can show the check button
# and we do NOT have a reset button, then we can show the check button
# Setting rerandomize to "never" or "false" ensures that the reset button
# Setting rerandomize to "never" or "false" ensures that the reset button
# is not shown
# is not shown
module
=
CapaFactory
.
create
(
rerandomize
=
"never"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
NEVER
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_check_button
())
self
.
assertTrue
(
module
.
should_show_check_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
"false"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
"false"
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_check_button
())
self
.
assertTrue
(
module
.
should_show_check_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
"per_student"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
PER_STUDENT
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_check_button
())
self
.
assertTrue
(
module
.
should_show_check_button
())
def
test_should_show_reset_button
(
self
):
def
test_should_show_reset_button
(
self
):
...
@@ -1101,30 +1107,36 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1101,30 +1107,36 @@ class CapaModuleTest(unittest.TestCase):
module
=
CapaFactory
.
create
(
attempts
=
attempts
,
max_attempts
=
attempts
,
done
=
True
)
module
=
CapaFactory
.
create
(
attempts
=
attempts
,
max_attempts
=
attempts
,
done
=
True
)
self
.
assertFalse
(
module
.
should_show_reset_button
())
self
.
assertFalse
(
module
.
should_show_reset_button
())
#
If we're NOT randomizing, then do NOT
show the reset button
#
pre studio default value, DO
show the reset button
module
=
CapaFactory
.
create
(
rerandomize
=
"never"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
done
=
True
)
self
.
assert
Fals
e
(
module
.
should_show_reset_button
())
self
.
assert
Tru
e
(
module
.
should_show_reset_button
())
# If we're NOT randomizing, then do NOT show the reset button
# If survey question for capa (max_attempts = 0),
module
=
CapaFactory
.
create
(
rerandomize
=
"per_student"
,
done
=
True
)
# DO show the reset button
self
.
assertFalse
(
module
.
should_show_reset_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
max_attempts
=
0
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_reset_button
())
# If we're NOT randomizing, then do NOT show the reset button
# If the question is not correct
module
=
CapaFactory
.
create
(
rerandomize
=
"false"
,
done
=
True
)
# DO show the reset button
self
.
assertFalse
(
module
.
should_show_reset_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
max_attempts
=
0
,
done
=
True
,
correct
=
False
)
self
.
assertTrue
(
module
.
should_show_reset_button
())
# If the
user hasn't submitted an answer yet,
# If the
question is correct and randomization is never
#
then do NOT
show the reset button
#
DO not
show the reset button
module
=
CapaFactory
.
create
(
done
=
Fals
e
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
NEVER
,
max_attempts
=
0
,
done
=
True
,
correct
=
Tru
e
)
self
.
assertFalse
(
module
.
should_show_reset_button
())
self
.
assertFalse
(
module
.
should_show_reset_button
())
# pre studio default value, DO show the reset button
# If the question is correct and randomization is always
module
=
CapaFactory
.
create
(
rerandomize
=
"always"
,
done
=
True
)
# Show the reset button
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
max_attempts
=
0
,
done
=
True
,
correct
=
True
)
self
.
assertTrue
(
module
.
should_show_reset_button
())
self
.
assertTrue
(
module
.
should_show_reset_button
())
# If survey question for capa (max_attempts = 0),
# Don't show reset button if randomization is turned on and the question is not done
# DO show the reset button
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
show_reset_button
=
False
,
done
=
False
)
module
=
CapaFactory
.
create
(
rerandomize
=
"always"
,
max_attempts
=
0
,
done
=
True
)
self
.
assertFalse
(
module
.
should_show_reset_button
())
# Show reset button if randomization is turned on and the problem is done
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
show_reset_button
=
False
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_reset_button
())
self
.
assertTrue
(
module
.
should_show_reset_button
())
def
test_should_show_save_button
(
self
):
def
test_should_show_save_button
(
self
):
...
@@ -1140,7 +1152,7 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1140,7 +1152,7 @@ class CapaModuleTest(unittest.TestCase):
self
.
assertFalse
(
module
.
should_show_save_button
())
self
.
assertFalse
(
module
.
should_show_save_button
())
# If user submitted a problem but hasn't reset, do NOT show the save button
# If user submitted a problem but hasn't reset, do NOT show the save button
module
=
CapaFactory
.
create
(
rerandomize
=
"always"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
done
=
True
)
self
.
assertFalse
(
module
.
should_show_save_button
())
self
.
assertFalse
(
module
.
should_show_save_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
"true"
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
"true"
,
done
=
True
)
...
@@ -1149,27 +1161,27 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1149,27 +1161,27 @@ class CapaModuleTest(unittest.TestCase):
# If the user has unlimited attempts and we are not randomizing,
# If the user has unlimited attempts and we are not randomizing,
# then do NOT show a save button
# then do NOT show a save button
# because they can keep using "Check"
# because they can keep using "Check"
module
=
CapaFactory
.
create
(
max_attempts
=
None
,
rerandomize
=
"never"
,
done
=
False
)
module
=
CapaFactory
.
create
(
max_attempts
=
None
,
rerandomize
=
RANDOMIZATION
.
NEVER
,
done
=
False
)
self
.
assertFalse
(
module
.
should_show_save_button
())
self
.
assertFalse
(
module
.
should_show_save_button
())
module
=
CapaFactory
.
create
(
max_attempts
=
None
,
rerandomize
=
"false"
,
done
=
True
)
module
=
CapaFactory
.
create
(
max_attempts
=
None
,
rerandomize
=
"false"
,
done
=
True
)
self
.
assertFalse
(
module
.
should_show_save_button
())
self
.
assertFalse
(
module
.
should_show_save_button
())
module
=
CapaFactory
.
create
(
max_attempts
=
None
,
rerandomize
=
"per_student"
,
done
=
True
)
module
=
CapaFactory
.
create
(
max_attempts
=
None
,
rerandomize
=
RANDOMIZATION
.
PER_STUDENT
,
done
=
True
)
self
.
assertFalse
(
module
.
should_show_save_button
())
self
.
assertFalse
(
module
.
should_show_save_button
())
# pre-studio default, DO show the save button
# pre-studio default, DO show the save button
module
=
CapaFactory
.
create
(
rerandomize
=
"always"
,
done
=
False
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
done
=
False
)
self
.
assertTrue
(
module
.
should_show_save_button
())
self
.
assertTrue
(
module
.
should_show_save_button
())
# If we're not randomizing and we have limited attempts, then we can save
# If we're not randomizing and we have limited attempts, then we can save
module
=
CapaFactory
.
create
(
rerandomize
=
"never"
,
max_attempts
=
2
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
NEVER
,
max_attempts
=
2
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_save_button
())
self
.
assertTrue
(
module
.
should_show_save_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
"false"
,
max_attempts
=
2
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
"false"
,
max_attempts
=
2
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_save_button
())
self
.
assertTrue
(
module
.
should_show_save_button
())
module
=
CapaFactory
.
create
(
rerandomize
=
"per_student"
,
max_attempts
=
2
,
done
=
True
)
module
=
CapaFactory
.
create
(
rerandomize
=
RANDOMIZATION
.
PER_STUDENT
,
max_attempts
=
2
,
done
=
True
)
self
.
assertTrue
(
module
.
should_show_save_button
())
self
.
assertTrue
(
module
.
should_show_save_button
())
# If survey question for capa (max_attempts = 0),
# If survey question for capa (max_attempts = 0),
...
@@ -1197,7 +1209,7 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1197,7 +1209,7 @@ class CapaModuleTest(unittest.TestCase):
# then show it even if we would ordinarily
# then show it even if we would ordinarily
# require a reset first
# require a reset first
module
=
CapaFactory
.
create
(
force_save_button
=
"true"
,
module
=
CapaFactory
.
create
(
force_save_button
=
"true"
,
rerandomize
=
"always"
,
rerandomize
=
RANDOMIZATION
.
ALWAYS
,
done
=
True
)
done
=
True
)
self
.
assertTrue
(
module
.
should_show_save_button
())
self
.
assertTrue
(
module
.
should_show_save_button
())
...
@@ -1331,48 +1343,66 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1331,48 +1343,66 @@ class CapaModuleTest(unittest.TestCase):
context
=
render_args
[
1
]
context
=
render_args
[
1
]
self
.
assertTrue
(
error_msg
in
context
[
'problem'
][
'html'
])
self
.
assertTrue
(
error_msg
in
context
[
'problem'
][
'html'
])
def
test_random_seed_no_change
(
self
):
@ddt.data
(
'false'
,
'true'
,
RANDOMIZATION
.
NEVER
,
RANDOMIZATION
.
PER_STUDENT
,
RANDOMIZATION
.
ALWAYS
,
RANDOMIZATION
.
ONRESET
)
def
test_random_seed_no_change
(
self
,
rerandomize
):
# Run the test for each possible rerandomize value
# Run the test for each possible rerandomize value
for
rerandomize
in
[
'false'
,
'never'
,
'per_student'
,
'always'
,
'true'
,
'onreset'
]:
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
)
# Get the seed
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
)
# By this point, the module should have persisted the seed
seed
=
module
.
seed
self
.
assertTrue
(
seed
is
not
None
)
# If we're not rerandomizing, the seed is always set
# Get the seed
# to the same value (1)
# By this point, the module should have persisted the seed
if
rerandomize
in
[
'never'
]:
seed
=
module
.
seed
self
.
assertEqual
(
seed
,
1
,
self
.
assertTrue
(
seed
is
not
None
)
msg
=
"Seed should always be 1 when rerandomize='
%
s'"
%
rerandomize
)
# Check the problem
# If we're not rerandomizing, the seed is always set
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
# to the same value (1)
module
.
check_problem
(
get_request_dict
)
if
rerandomize
==
RANDOMIZATION
.
NEVER
:
self
.
assertEqual
(
seed
,
1
,
msg
=
"Seed should always be 1 when rerandomize='
%
s'"
%
rerandomize
)
# Expect that the seed is the same
# Check the problem
self
.
assertEqual
(
seed
,
module
.
seed
)
get_request_dict
=
{
CapaFactory
.
input_key
():
'3.14'
}
module
.
check_problem
(
get_request_dict
)
# Save the problem
# Expect that the seed is the same
module
.
save_problem
(
get_request_dict
)
self
.
assertEqual
(
seed
,
module
.
seed
)
# Expect that the seed is the same
# Save the problem
self
.
assertEqual
(
seed
,
module
.
seed
)
module
.
save_problem
(
get_request_dict
)
# Expect that the seed is the same
self
.
assertEqual
(
seed
,
module
.
seed
)
@ddt.data
(
'false'
,
'true'
,
RANDOMIZATION
.
NEVER
,
RANDOMIZATION
.
PER_STUDENT
,
RANDOMIZATION
.
ALWAYS
,
RANDOMIZATION
.
ONRESET
)
def
test_random_seed_with_reset
(
self
,
rerandomize
):
"""
Run the test for each possible rerandomize value
"""
def
test_random_seed_with_reset
(
self
):
def
_reset_and_get_seed
(
module
):
def
_reset_and_get_seed
(
module
):
'''
"""
Reset the XModule and return the module's seed
Reset the XModule and return the module's seed
'''
"""
# Simulate submitting an attempt
# Simulate submitting an attempt
# We need to do this, or reset_problem() will
# We need to do this, or reset_problem() will
# fail
with a complaint that we haven't
submitted
# fail
because it won't re-randomize until the problem has been
submitted
# the problem yet.
# the problem yet.
module
.
done
=
True
module
.
done
=
True
...
@@ -1397,45 +1427,83 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -1397,45 +1427,83 @@ class CapaModuleTest(unittest.TestCase):
break
break
return
success
return
success
# Run the test for each possible rerandomize value
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
done
=
True
)
for
rerandomize
in
[
'never'
,
'false'
,
'per_student'
,
'always'
,
'true'
,
'onreset'
]:
# Get the seed
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
)
# By this point, the module should have persisted the seed
seed
=
module
.
seed
self
.
assertTrue
(
seed
is
not
None
)
# We do NOT want the seed to reset if rerandomize
# is set to 'never' -- it should still be 1
# The seed also stays the same if we're randomizing
# 'per_student': the same student should see the same problem
if
rerandomize
in
[
RANDOMIZATION
.
NEVER
,
'false'
,
RANDOMIZATION
.
PER_STUDENT
]:
self
.
assertEqual
(
seed
,
_reset_and_get_seed
(
module
))
# Otherwise, we expect the seed to change
# to another valid seed
else
:
# Since there's a small chance we might get the
# same seed again, give it 5 chances
# to generate a different seed
success
=
_retry_and_check
(
5
,
lambda
:
_reset_and_get_seed
(
module
)
!=
seed
)
self
.
assertTrue
(
module
.
seed
is
not
None
)
msg
=
'Could not get a new seed from reset after 5 tries'
self
.
assertTrue
(
success
,
msg
)
@ddt.data
(
'false'
,
'true'
,
RANDOMIZATION
.
NEVER
,
RANDOMIZATION
.
PER_STUDENT
,
RANDOMIZATION
.
ALWAYS
,
RANDOMIZATION
.
ONRESET
)
def
test_random_seed_with_reset_question_unsubmitted
(
self
,
rerandomize
):
"""
Run the test for each possible rerandomize value
"""
def
_reset_and_get_seed
(
module
):
"""
Reset the XModule and return the module's seed
"""
# Get the seed
# Reset the problem
# By this point, the module should have persisted the seed
# By default, the problem is instantiated as unsubmitted
seed
=
module
.
seed
module
.
reset_problem
({})
self
.
assertTrue
(
seed
is
not
None
)
# We do NOT want the seed to reset if rerandomize
# Return the seed
# is set to 'never' -- it should still be 1
return
module
.
seed
# The seed also stays the same if we're randomizing
# 'per_student': the same student should see the same problem
if
rerandomize
in
[
'never'
,
'false'
,
'per_student'
]:
self
.
assertEqual
(
seed
,
_reset_and_get_seed
(
module
))
# Otherwise, we expect the seed to change
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
,
done
=
False
)
# to another valid seed
else
:
# Since there's a small chance we might get the
# Get the seed
# same seed again, give it 5 chances
# By this point, the module should have persisted the seed
# to generate a different
seed
seed
=
module
.
seed
success
=
_retry_and_check
(
5
,
lambda
:
_reset_and_get_seed
(
module
)
!=
seed
)
self
.
assertTrue
(
seed
is
not
None
)
self
.
assertTrue
(
module
.
seed
is
not
None
)
#the seed should never change because the student hasn't finished the problem
msg
=
'Could not get a new seed from reset after 5 tries'
self
.
assertEqual
(
seed
,
_reset_and_get_seed
(
module
))
self
.
assertTrue
(
success
,
msg
)
def
test_random_seed_bins
(
self
):
@ddt.data
(
RANDOMIZATION
.
ALWAYS
,
RANDOMIZATION
.
PER_STUDENT
,
'true'
,
RANDOMIZATION
.
ONRESET
)
def
test_random_seed_bins
(
self
,
rerandomize
):
# Assert that we are limiting the number of possible seeds.
# Assert that we are limiting the number of possible seeds.
# Get a bunch of seeds, they should all be in 0-999.
# Check the conditions that generate random seeds
i
=
200
for
rerandomize
in
[
'always'
,
'per_student'
,
'true'
,
'onreset'
]:
while
i
>
0
:
# Get a bunch of seeds, they should all be in 0-999.
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
)
for
i
in
range
(
200
):
assert
0
<=
module
.
seed
<
1000
module
=
CapaFactory
.
create
(
rerandomize
=
rerandomize
)
i
-=
1
assert
0
<=
module
.
seed
<
1000
@patch
(
'xmodule.capa_base.log'
)
@patch
(
'xmodule.capa_base.log'
)
@patch
(
'xmodule.capa_base.Progress'
)
@patch
(
'xmodule.capa_base.Progress'
)
...
@@ -1765,7 +1833,7 @@ class TestProblemCheckTracking(unittest.TestCase):
...
@@ -1765,7 +1833,7 @@ class TestProblemCheckTracking(unittest.TestCase):
def
test_rerandomized_inputs
(
self
):
def
test_rerandomized_inputs
(
self
):
factory
=
CapaFactory
factory
=
CapaFactory
module
=
factory
.
create
(
rerandomize
=
'always'
)
module
=
factory
.
create
(
rerandomize
=
RANDOMIZATION
.
ALWAYS
)
answer_input_dict
=
{
answer_input_dict
=
{
factory
.
input_key
(
2
):
'3.14'
factory
.
input_key
(
2
):
'3.14'
...
...
lms/djangoapps/courseware/features/problems.feature
View file @
3d1c54fe
...
@@ -72,37 +72,77 @@ Feature: LMS.Answer problems
...
@@ -72,37 +72,77 @@ Feature: LMS.Answer problems
Scenario
:
I
can reset a problem
Scenario
:
I
can reset a problem
Given
I am viewing a
"<ProblemType>"
problem
Given
I am viewing a randomization
"<Randomization>"
"<ProblemType>"
problem with reset button on
And
I answer a
"<ProblemType>"
problem
"<Correctness>ly"
When
I reset the problem
Then
my
"<ProblemType>"
answer is marked
"unanswered"
And
The
"<ProblemType>"
problem displays a
"blank"
answer
Examples
:
|
ProblemType
|
Correctness
|
Randomization
|
|
drop
down
|
correct
|
always
|
|
drop
down
|
incorrect
|
always
|
|
multiple
choice
|
correct
|
always
|
|
multiple
choice
|
incorrect
|
always
|
|
checkbox
|
correct
|
always
|
|
checkbox
|
incorrect
|
always
|
|
radio
|
correct
|
always
|
|
radio
|
incorrect
|
always
|
|
string
|
correct
|
always
|
|
string
|
incorrect
|
always
|
|
numerical
|
correct
|
always
|
|
numerical
|
incorrect
|
always
|
|
formula
|
correct
|
always
|
|
formula
|
incorrect
|
always
|
|
script
|
correct
|
always
|
|
script
|
incorrect
|
always
|
|
radio_text
|
correct
|
always
|
|
radio_text
|
incorrect
|
always
|
|
checkbox_text
|
correct
|
always
|
|
checkbox_text
|
incorrect
|
always
|
|
image
|
correct
|
always
|
|
image
|
incorrect
|
always
|
Scenario
:
I
can reset a non-randomized problem that I answer incorrectly
Given
I am viewing a randomization
"<Randomization>"
"<ProblemType>"
problem with reset button on
And
I answer a
"<ProblemType>"
problem
"<Correctness>ly"
And
I answer a
"<ProblemType>"
problem
"<Correctness>ly"
When
I reset the problem
When
I reset the problem
Then
my
"<ProblemType>"
answer is marked
"unanswered"
Then
my
"<ProblemType>"
answer is marked
"unanswered"
And
The
"<ProblemType>"
problem displays a
"blank"
answer
And
The
"<ProblemType>"
problem displays a
"blank"
answer
Examples
:
Examples
:
|
ProblemType
|
Correctness
|
|
ProblemType
|
Correctness
|
Randomization
|
|
drop
down
|
correct
|
|
drop
down
|
incorrect
|
never
|
|
drop
down
|
incorrect
|
|
drop
down
|
incorrect
|
never
|
|
multiple
choice
|
correct
|
|
multiple
choice
|
incorrect
|
never
|
|
multiple
choice
|
incorrect
|
|
checkbox
|
incorrect
|
never
|
|
checkbox
|
correct
|
|
radio
|
incorrect
|
never
|
|
checkbox
|
incorrect
|
|
string
|
incorrect
|
never
|
|
radio
|
correct
|
|
numerical
|
incorrect
|
never
|
|
radio
|
incorrect
|
|
formula
|
incorrect
|
never
|
|
string
|
correct
|
|
script
|
incorrect
|
never
|
|
string
|
incorrect
|
|
radio_text
|
incorrect
|
never
|
|
numerical
|
correct
|
|
checkbox_text
|
incorrect
|
never
|
|
numerical
|
incorrect
|
|
image
|
incorrect
|
never
|
|
formula
|
correct
|
|
formula
|
incorrect
|
Scenario
:
The reset button doesn't show up
|
script
|
correct
|
Given
I am viewing a randomization
"<Randomization>"
"<ProblemType>"
problem with reset button on
|
script
|
incorrect
|
And
I answer a
"<ProblemType>"
problem
"<Correctness>ly"
|
radio_text
|
correct
|
Then
The
"Reset"
button does not appear
|
radio_text
|
incorrect
|
|
checkbox_text
|
correct
|
|
checkbox_text
|
incorrect
|
|
image
|
correct
|
|
image
|
incorrect
|
Examples
:
|
ProblemType
|
Correctness
|
Randomization
|
|
drop
down
|
correct
|
never
|
|
multiple
choice
|
correct
|
never
|
|
checkbox
|
correct
|
never
|
|
radio
|
correct
|
never
|
|
string
|
correct
|
never
|
|
numerical
|
correct
|
never
|
|
formula
|
correct
|
never
|
|
script
|
correct
|
never
|
|
radio_text
|
correct
|
never
|
|
checkbox_text
|
correct
|
never
|
|
image
|
correct
|
never
|
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
Given
I am viewing a
"multiple choice"
problem with
"1"
attempt
Given
I am viewing a
"multiple choice"
problem with
"1"
attempt
...
@@ -115,6 +155,12 @@ Feature: LMS.Answer problems
...
@@ -115,6 +155,12 @@ Feature: LMS.Answer problems
When
I answer a
"multiple choice"
problem
"correctly"
When
I answer a
"multiple choice"
problem
"correctly"
Then
The
"Reset"
button does appear
Then
The
"Reset"
button does appear
Scenario
:
I
can answer a problem with multiple attempts correctly but cannot reset because randomization is off
Given
I am viewing a randomization
"never"
"multiple choice"
problem with
"3"
attempts with reset
Then
I should see
"You have used 0 of 3 submissions"
somewhere in the page
When
I answer a
"multiple choice"
problem
"correctly"
Then
The
"Reset"
button does not appear
Scenario
:
I
can view how many attempts I have left on a problem
Scenario
:
I
can view how many attempts I have left on a problem
Given
I am viewing a
"multiple choice"
problem with
"3"
attempts
Given
I am viewing a
"multiple choice"
problem with
"3"
attempts
Then
I should see
"You have used 0 of 3 submissions"
somewhere in the page
Then
I should see
"You have used 0 of 3 submissions"
somewhere in the page
...
@@ -165,6 +211,34 @@ Feature: LMS.Answer problems
...
@@ -165,6 +211,34 @@ Feature: LMS.Answer problems
|
image
|
correct
|
1/1
point
|
1
point
possible
|
|
image
|
correct
|
1/1
point
|
1
point
possible
|
|
image
|
incorrect
|
1
point
possible
|
1
point
possible
|
|
image
|
incorrect
|
1
point
possible
|
1
point
possible
|
Scenario
:
I
can see my score on a problem when I answer it and after I reset it
Given
I am viewing a
"<ProblemType>"
problem with randomization
"<Randomization>"
with reset button on
When
I answer a
"<ProblemType>"
problem
"<Correctness>ly"
Then
I should see a score of
"<Score>"
When
I reset the problem
Then
I should see a score of
"<Points Possible>"
Examples
:
|
ProblemType
|
Correctness
|
Score
|
Points
Possible
|
Randomization
|
|
drop
down
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
drop
down
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
multiple
choice
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
multiple
choice
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
checkbox
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
checkbox
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
radio
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
radio
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
string
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
string
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
numerical
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
numerical
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
formula
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
formula
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
|
script
|
correct
|
2/2
points
|
2
points
possible
|
never
|
|
script
|
incorrect
|
2
points
possible
|
2
points
possible
|
never
|
|
image
|
correct
|
1/1
point
|
1
point
possible
|
never
|
|
image
|
incorrect
|
1
point
possible
|
1
point
possible
|
never
|
Scenario
:
I
can see my score on a problem to which I submit a blank answer
Scenario
:
I
can see my score on a problem to which I submit a blank answer
Given
I am viewing a
"<ProblemType>"
problem
Given
I am viewing a
"<ProblemType>"
problem
When
I check a problem
When
I check a problem
...
...
lms/djangoapps/courseware/features/problems.py
View file @
3d1c54fe
...
@@ -26,6 +26,13 @@ def view_problem_with_attempts(step, problem_type, attempts):
...
@@ -26,6 +26,13 @@ def view_problem_with_attempts(step, problem_type, attempts):
_view_problem
(
step
,
problem_type
,
{
'max_attempts'
:
attempts
})
_view_problem
(
step
,
problem_type
,
{
'max_attempts'
:
attempts
})
@step
(
u'I am viewing a randomization "([^"]*)" "([^"]*)" problem with "([^"]*)" attempts with reset'
)
def
view_problem_attempts_reset
(
step
,
randomization
,
problem_type
,
attempts
,
):
_view_problem
(
step
,
problem_type
,
{
'max_attempts'
:
attempts
,
'rerandomize'
:
randomization
,
'show_reset_button'
:
True
})
@step
(
u'I am viewing a "([^"]*)" that shows the answer "([^"]*)"'
)
@step
(
u'I am viewing a "([^"]*)" that shows the answer "([^"]*)"'
)
def
view_problem_with_show_answer
(
step
,
problem_type
,
answer
):
def
view_problem_with_show_answer
(
step
,
problem_type
,
answer
):
_view_problem
(
step
,
problem_type
,
{
'showanswer'
:
answer
})
_view_problem
(
step
,
problem_type
,
{
'showanswer'
:
answer
})
...
@@ -36,6 +43,11 @@ def view_problem(step, problem_type):
...
@@ -36,6 +43,11 @@ def view_problem(step, problem_type):
_view_problem
(
step
,
problem_type
)
_view_problem
(
step
,
problem_type
)
@step
(
u'I am viewing a randomization "([^"]*)" "([^"]*)" problem with reset button on'
)
def
view_random_reset_problem
(
step
,
randomization
,
problem_type
):
_view_problem
(
step
,
problem_type
,
{
'rerandomize'
:
randomization
,
'show_reset_button'
:
True
})
@step
(
u'External graders respond "([^"]*)"'
)
@step
(
u'External graders respond "([^"]*)"'
)
def
set_external_grader_response
(
step
,
correctness
):
def
set_external_grader_response
(
step
,
correctness
):
assert
(
correctness
in
[
'correct'
,
'incorrect'
])
assert
(
correctness
in
[
'correct'
,
'incorrect'
])
...
...
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