Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/features/problem-editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +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$')
def i_created_blank_common_problem(step):
Expand Down Expand Up @@ -45,6 +45,7 @@ def i_see_advanced_settings_with_values(step):
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", False],
[SHOW_ANSWER, "Finished", False],
[TIMER_BETWEEN_ATTEMPTS, "0", False]
])


Expand Down
63 changes: 56 additions & 7 deletions common/lib/xmodule/xmodule/capa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. "
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -894,7 +908,7 @@ def publish_grade(self):

return {'grade': score['score'], 'max_grade': score['total']}

def check_problem(self, data):
def check_problem(self, data, override_time=False):
"""
Checks whether answers to a problem are correct

Expand All @@ -909,6 +923,11 @@ def check_problem(self, data):
answers = self.make_dict_of_responses(data)
event_info['answers'] = convert_files_to_filenames(answers)

# Can override current time
current_time = datetime.datetime.now(UTC())
if override_time is not False:
current_time = override_time

# Too late. Cannot submit
if self.closed():
event_info['failure'] = 'closed'
Expand All @@ -923,23 +942,31 @@ def check_problem(self, data):

# Problem queued. Students must wait a specified waittime before they are allowed to submit
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
if self.last_submission_time is not None and self.submission_wait_seconds != 0:
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())
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",
exc_info=True)
log.warning("StudentInputError in capa_module:problem_check", exc_info=True)

# Save the user's state before failing
self.set_state_from_lcp()
Expand Down Expand Up @@ -990,9 +1017,31 @@ def check_problem(self, data):
# render problem into HTML
html = self.get_problem_html(encapsulate=False)

return {'success': success,
'contents': html,
}
return {'success': success, '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):
min_display = int(num_seconds / 60)
sec_display = num_seconds % 60
plural = "s" if min_display > 1 else ""
if sec_display == 0:
return "%i minute%s" % (min_display, plural)
else:
return "%i min, %i sec" % (min_display, sec_display)
else:
hr_display = int(num_seconds / 3600)
min_display = int((num_seconds % 3600) / 60)
sec_display = num_seconds % 60
if sec_display == 0:
return "%i hr, %i min" % (hr_display, min_display)
else:
return "%i hr, %i min, %i sec" % (hr_display, min_display, sec_display)

def rescore_problem(self):
"""
Expand Down
Loading