diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a62b0fb0091e..3a837cf22cf8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -118,6 +118,13 @@ client error are correctly passed through to the client.
LMS: Improve performance of page load and thread list load for
discussion tab
+Studio: Added feature to allow instructors to specify wait time between attempts
+of the same quiz. In a problem's settings, instructors can specify how many
+seconds student's are locked out of submitting another attempt of the same quiz.
+The timer starts as soon as they submit an attempt for grading. Note that this
+does not prevent a student from starting to work on another quiz attempt. It only
+prevents the students from submitting a bunch of attempts in rapid succession.
+
LMS: The wiki markup cheatsheet dialog is now accessible to screen readers.
(LMS-1303)
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
index 2265b5010e4c..6f4233ee57c5 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ b/cms/djangoapps/contentstore/features/problem-editor.py
@@ -14,6 +14,7 @@
PROBLEM_WEIGHT = "Problem Weight"
RANDOMIZATION = 'Randomization'
SHOW_ANSWER = "Show Answer"
+TIMER_BETWEEN_ATTEMPTS = "Timer Between Attempts"
@step('I have created a Blank Common Problem$')
@@ -45,6 +46,7 @@ def i_see_advanced_settings_with_values(step):
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", False],
[SHOW_ANSWER, "Finished", False],
+ [TIMER_BETWEEN_ATTEMPTS, "0", False]
])
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index b78e2a4a5019..9e9b4fed3278 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -141,6 +141,14 @@ class CapaFields(object):
student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = Integer(help="Random seed for this student", scope=Scope.user_state)
+
+ last_submission_time = Date(help="Last submission time", scope=Scope.user_state)
+ submission_wait_seconds = Integer(
+ display_name="Timer Between Attempts",
+ help="Seconds a student must wait between submissions for a problem with multiple attempts.",
+ scope=Scope.settings,
+ default=0)
+
weight = Float(
display_name="Problem Weight",
help=("Defines the number of points each problem is worth. "
@@ -303,6 +311,12 @@ def set_state_from_lcp(self):
self.student_answers = lcp_state['student_answers']
self.seed = lcp_state['seed']
+ def set_last_submission_time(self):
+ """
+ Set the module's last submission time (when the problem was checked)
+ """
+ self.last_submission_time = datetime.datetime.now(UTC())
+
def get_score(self):
"""
Access the problem's score
@@ -925,17 +939,28 @@ def check_problem(self, data):
if self.lcp.is_queued():
current_time = datetime.datetime.now(UTC())
prev_submit_time = self.lcp.get_recentmost_queuetime()
+
waittime_between_requests = self.system.xqueue['waittime']
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
msg = u'You must wait at least {wait} seconds between submissions'.format(
wait=waittime_between_requests)
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
+ # Wait time between resets
+ current_time = datetime.datetime.now(UTC())
+ if self.last_submission_time is not None:
+ if (current_time - self.last_submission_time).total_seconds() < self.submission_wait_seconds:
+ seconds_left = int(self.submission_wait_seconds - (current_time - self.last_submission_time).total_seconds()) + 1
+ msg = u'You must wait at least {w} between submissions. {s} remaining.'.format(
+ w=self.pretty_print_seconds(self.submission_wait_seconds), s=self.pretty_print_seconds(seconds_left))
+ return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
+
try:
correct_map = self.lcp.grade_answers(answers)
self.attempts = self.attempts + 1
self.lcp.done = True
self.set_state_from_lcp()
+ self.set_last_submission_time()
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
log.warning("StudentInputError in capa_module:problem_check",
@@ -994,6 +1019,18 @@ def check_problem(self, data):
'contents': html,
}
+ def pretty_print_seconds(self, num_seconds):
+ """
+ Returns time formatted nicely.
+ """
+ if(num_seconds < 60):
+ plural = "s" if num_seconds > 1 else ""
+ return "%i second%s" % (num_seconds, plural)
+ elif(num_seconds < 60*60):
+ return "%i min, %i sec" % (int(num_seconds / 60), num_seconds % 60)
+ else:
+ return "%i hrs, %i min, %i sec" % (int(num_seconds / 3600), int((num_seconds % 3600) / 60), (num_seconds % 60))
+
def rescore_problem(self):
"""
Checks whether the existing answers to a problem are correct.
diff --git a/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py b/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py
new file mode 100644
index 000000000000..69079be5daff
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py
@@ -0,0 +1,733 @@
+"""
+Tests the logic of problems with a delay between attempt submissions
+"""
+
+import unittest
+import textwrap
+import datetime
+import json
+import random
+import os
+import textwrap
+import unittest
+
+from mock import Mock, patch
+import webob
+from webob.multidict import MultiDict
+
+import xmodule
+from xmodule.tests import DATA_DIR
+from capa.responsetypes import (StudentInputError, LoncapaProblemError,
+ ResponseError)
+from capa.xqueue_interface import XQueueInterface
+from xmodule.capa_module import CapaModule, ComplexEncoder
+from xmodule.modulestore import Location
+from xblock.field_data import DictFieldData
+from xblock.fields import ScopeIds
+
+from . import get_test_system
+from pytz import UTC
+from capa.correctmap import CorrectMap
+
+
+class CapaFactory(object):
+ """
+ A helper class to create problem modules with various parameters for testing.
+ """
+
+ sample_problem_xml = textwrap.dedent("""\
+
+ What is pi, to two decimal places?
What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 2nd WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 2nd WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 2nd WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 2nd WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 2nd WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 2nd WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Targeted Feedback
+ #Feedback on the other solution...
+ #Explanation
+ #This is the other solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #What is the correct answer?
+ #Targeted Feedback
+ #This is the 1st WRONG solution
+ #Targeted Feedback
+ #This is the 3rd WRONG solution
+ #Targeted Feedback
+ #Feedback on your correct solution...
+ #Explanation
+ #This is the solution explanation
+ #Not much to explain here, sorry!
+ #