Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

MDL-35717 quiz: fix overdue attempt processing

  • Loading branch information...
commit 8e771aed93eea08cc3e9410283f5354e02311281 1 parent 6109f21
@mpetrowi mpetrowi authored
Showing with 1,069 additions and 164 deletions.
  1. +22 −4 mod/quiz/accessmanager.php
  2. +18 −4 mod/quiz/accessrule/accessrulebase.php
  3. +10 −5 mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php
  4. +4 −2 mod/quiz/accessrule/ipaddress/tests/rule_test.php
  5. +2 −1  mod/quiz/accessrule/numattempts/tests/rule_test.php
  6. +13 −9 mod/quiz/accessrule/openclosedate/rule.php
  7. +25 −17 mod/quiz/accessrule/openclosedate/tests/rule_test.php
  8. +2 −1  mod/quiz/accessrule/password/tests/rule_test.php
  9. +2 −1  mod/quiz/accessrule/safebrowser/tests/rule_test.php
  10. +2 −1  mod/quiz/accessrule/securewindow/tests/rule_test.php
  11. +12 −2 mod/quiz/accessrule/timelimit/rule.php
  12. +5 −3 mod/quiz/accessrule/timelimit/tests/rule_test.php
  13. +4 −0 mod/quiz/accessrule/upgrade.txt
  14. +39 −8 mod/quiz/attemptlib.php
  15. +1 −1  mod/quiz/backup/moodle2/backup_quiz_stepslib.php
  16. +1 −0  mod/quiz/backup/moodle2/restore_quiz_stepslib.php
  17. +20 −56 mod/quiz/cronlib.php
  18. +23 −0 mod/quiz/db/events.php
  19. +10 −8 mod/quiz/db/install.xml
  20. +32 −0 mod/quiz/db/upgrade.php
  21. +30 −22 mod/quiz/lib.php
  22. +198 −2 mod/quiz/locallib.php
  23. +14 −3 mod/quiz/module.js
  24. +1 −0  mod/quiz/overrideedit.php
  25. +9 −4 mod/quiz/processattempt.php
  26. +12 −8 mod/quiz/renderer.php
  27. +1 −1  mod/quiz/startattempt.php
  28. +388 −0 mod/quiz/tests/attempts_test.php
  29. +105 −0 mod/quiz/tests/generator/lib.php
  30. +63 −0 mod/quiz/tests/generator_test.php
  31. +1 −1  mod/quiz/version.php
View
26 mod/quiz/accessmanager.php
@@ -392,17 +392,35 @@ public function setup_attempt_page($page) {
}
/**
- * Compute how much time is left before this attempt must be submitted.
+ * Compute when the attempt must be submitted.
+ *
+ * @param object $attempt the data from the relevant quiz_attempts row.
+ * @return int|false the attempt close time.
+ * False if there is no limit.
+ */
+ public function get_end_time($attempt) {
+ $timeclose = false;
+ foreach ($this->rules as $rule) {
+ $ruletimeclose = $rule->end_time($attempt);
+ if ($ruletimeclose !== false && ($timeclose === false || $ruletimeclose < $timeclose)) {
+ $timeclose = $ruletimeclose;
+ }
+ }
+ return $timeclose;
+ }
+
+ /**
+ * Compute what should be displayed to the user for time remaining in this attempt.
*
* @param object $attempt the data from the relevant quiz_attempts row.
* @param int $timenow the time to consider as 'now'.
* @return int|false the number of seconds remaining for this attempt.
- * False if there is no limit.
+ * False if no limit should be displayed.
*/
- public function get_time_left($attempt, $timenow) {
+ public function get_time_left_display($attempt, $timenow) {
$timeleft = false;
foreach ($this->rules as $rule) {
- $ruletimeleft = $rule->time_left($attempt, $timenow);
+ $ruletimeleft = $rule->time_left_display($attempt, $timenow);
if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) {
$timeleft = $ruletimeleft;
}
View
22 mod/quiz/accessrule/accessrulebase.php
@@ -180,14 +180,28 @@ public function is_finished($numprevattempts, $lastattempt) {
/**
* If, because of this rule, the user has to finish their attempt by a certain time,
- * you should override this method to return the amount of time left in seconds.
+ * you should override this method to return the attempt end time.
+ * @param object $attempt the current attempt
+ * @return mixed the attempt close time, or false if there is no close time.
+ */
+ public function end_time($attempt) {
+ return false;
+ }
+
+ /**
+ * If the user should be shown a different amount of time than $timenow - $this->end_time(), then
+ * override this method. This is useful if the time remaining is large enough to be omitted.
* @param object $attempt the current attempt
* @param int $timenow the time now. We don't use $this->timenow, so we can
* give the user a more accurate indication of how much time is left.
- * @return mixed false if there is no deadline, of the time left in seconds if there is one.
+ * @return mixed the time left in seconds (can be negative) or false if there is no limit.
*/
- public function time_left($attempt, $timenow) {
- return false;
+ public function time_left_display($attempt, $timenow) {
+ $endtime = $this->end_time($attempt);
+ if ($endtime === false) {
+ return false;
+ }
+ return $endtime - $timenow;
}
/**
View
15 mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php
@@ -55,7 +55,8 @@ public function test_just_first_delay() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
@@ -89,7 +90,8 @@ public function test_just_second_delay() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
@@ -128,7 +130,8 @@ public function test_just_both_delays() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
@@ -179,7 +182,8 @@ public function test_with_close_date() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$attempt->timefinish = 13000;
$this->assertEquals($rule->prevent_new_attempt(1, $attempt),
@@ -236,7 +240,8 @@ public function test_time_limit_and_overdue() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
View
6 mod/quiz/accessrule/ipaddress/tests/rule_test.php
@@ -56,7 +56,8 @@ public function test_ipaddress_access_rule() {
$this->assertFalse($rule->description());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 1));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
$quiz->subnet = '0.0.0.0';
@@ -68,6 +69,7 @@ public function test_ipaddress_access_rule() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 1));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
}
View
3  mod/quiz/accessrule/numattempts/tests/rule_test.php
@@ -64,6 +64,7 @@ public function test_num_attempts_access_rule() {
$this->assertTrue($rule->is_finished(666, $attempt));
$this->assertFalse($rule->prevent_access());
- $this->assertFalse($rule->time_left($attempt, 1));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
}
View
22 mod/quiz/accessrule/openclosedate/rule.php
@@ -93,20 +93,24 @@ public function is_finished($numprevattempts, $lastattempt) {
return $this->quiz->timeclose && $this->timenow > $this->quiz->timeclose;
}
- public function time_left($attempt, $timenow) {
+ public function end_time($attempt) {
+ if ($this->quiz->timeclose) {
+ return $this->quiz->timeclose;
+ }
+ return false;
+ }
+
+ public function time_left_display($attempt, $timenow) {
// If this is a teacher preview after the close date, do not show
// the time.
if ($attempt->preview && $timenow > $this->quiz->timeclose) {
return false;
}
-
- // Otherwise, return to the time left until the close date, providing
- // that is less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
- if ($this->quiz->timeclose) {
- $timeleft = $this->quiz->timeclose - $timenow;
- if ($timeleft < QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
- return $timeleft;
- }
+ // Otherwise, return to the time left until the close date, providing that is
+ // less than QUIZ_SHOW_TIME_BEFORE_DEADLINE.
+ $endtime = $this->end_time($attempt);
+ if ($endtime !== false && $timenow > $endtime - QUIZ_SHOW_TIME_BEFORE_DEADLINE) {
+ return $endtime - $timenow;
}
return false;
}
View
42 mod/quiz/accessrule/openclosedate/tests/rule_test.php
@@ -55,15 +55,17 @@ public function test_no_dates() {
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 10000));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 10000));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$rule = new quizaccess_openclosedate($quizobj, 0);
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
public function test_start_date() {
@@ -85,7 +87,8 @@ public function test_start_date() {
get_string('notavailable', 'quizaccess_openclosedate'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
$rule = new quizaccess_openclosedate($quizobj, 10000);
$this->assertEquals($rule->description(),
@@ -93,7 +96,8 @@ public function test_start_date() {
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 0));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
public function test_close_date() {
@@ -114,10 +118,12 @@ public function test_close_date() {
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
- $this->assertEquals($rule->time_left($attempt, 19900), 100);
- $this->assertEquals($rule->time_left($attempt, 20000), 0);
- $this->assertEquals($rule->time_left($attempt, 20100), -100);
+
+ $this->assertEquals($rule->end_time($attempt), 20000);
+ $this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
+ $this->assertEquals($rule->time_left_display($attempt, 19900), 100);
+ $this->assertEquals($rule->time_left_display($attempt, 20000), 0);
+ $this->assertEquals($rule->time_left_display($attempt, 20100), -100);
$rule = new quizaccess_openclosedate($quizobj, 20001);
$this->assertEquals($rule->description(),
@@ -126,10 +132,11 @@ public function test_close_date() {
get_string('notavailable', 'quizaccess_openclosedate'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertTrue($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
- $this->assertEquals($rule->time_left($attempt, 19900), 100);
- $this->assertEquals($rule->time_left($attempt, 20000), 0);
- $this->assertEquals($rule->time_left($attempt, 20100), -100);
+ $this->assertEquals($rule->end_time($attempt), 20000);
+ $this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
+ $this->assertEquals($rule->time_left_display($attempt, 19900), 100);
+ $this->assertEquals($rule->time_left_display($attempt, 20000), 0);
+ $this->assertEquals($rule->time_left_display($attempt, 20100), -100);
}
public function test_both_dates() {
@@ -176,10 +183,11 @@ public function test_both_dates() {
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertTrue($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
- $this->assertEquals($rule->time_left($attempt, 19900), 100);
- $this->assertEquals($rule->time_left($attempt, 20000), 0);
- $this->assertEquals($rule->time_left($attempt, 20100), -100);
+ $this->assertEquals($rule->end_time($attempt), 20000);
+ $this->assertFalse($rule->time_left_display($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
+ $this->assertEquals($rule->time_left_display($attempt, 19900), 100);
+ $this->assertEquals($rule->time_left_display($attempt, 20000), 0);
+ $this->assertEquals($rule->time_left_display($attempt, 20100), -100);
}
public function test_close_date_with_overdue() {
View
3  mod/quiz/accessrule/password/tests/rule_test.php
@@ -53,6 +53,7 @@ public function test_password_access_rule() {
get_string('requirepasswordmessage', 'quizaccess_password'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 1));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
}
View
3  mod/quiz/accessrule/safebrowser/tests/rule_test.php
@@ -58,6 +58,7 @@ public function test_safebrowser_access_rule() {
$rule->description());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 1));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
}
View
3  mod/quiz/accessrule/securewindow/tests/rule_test.php
@@ -54,6 +54,7 @@ public function test_securewindow_access_rule() {
$this->assertEmpty($rule->description());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
- $this->assertFalse($rule->time_left($attempt, 1));
+ $this->assertFalse($rule->end_time($attempt));
+ $this->assertFalse($rule->time_left_display($attempt, 0));
}
}
View
14 mod/quiz/accessrule/timelimit/rule.php
@@ -52,7 +52,17 @@ public function description() {
format_time($this->quiz->timelimit));
}
- public function time_left($attempt, $timenow) {
- return $attempt->timestart + $this->quiz->timelimit - $timenow;
+ public function end_time($attempt) {
+ return $attempt->timestart + $this->quiz->timelimit;
+ }
+
+ public function time_left_display($attempt, $timenow) {
+ // If this is a teacher preview after the time limit expires, don't show the time_left
+ $endtime = $this->end_time($attempt);
+ if ($attempt->preview && $timenow > $endtime) {
+ return false;
+ }
+ return $endtime - $timenow;
+
}
}
View
8 mod/quiz/accessrule/timelimit/tests/rule_test.php
@@ -51,9 +51,11 @@ public function test_time_limit_access_rule() {
get_string('quiztimelimit', 'quizaccess_timelimit', format_time(3600)));
$attempt->timestart = 10000;
- $this->assertEquals($rule->time_left($attempt, 10000), 3600);
- $this->assertEquals($rule->time_left($attempt, 12000), 1600);
- $this->assertEquals($rule->time_left($attempt, 14000), -400);
+ $attempt->preview = 0;
+ $this->assertEquals($rule->end_time($attempt), 13600);
+ $this->assertEquals($rule->time_left_display($attempt, 10000), 3600);
+ $this->assertEquals($rule->time_left_display($attempt, 12000), 1600);
+ $this->assertEquals($rule->time_left_display($attempt, 14000), -400);
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
View
4 mod/quiz/accessrule/upgrade.txt
@@ -13,3 +13,7 @@ Overview of this plugin type at http://docs.moodle.org/dev/Quiz_access_rules
* This plugin type now supports cron in the standard way. If required, Create a
lib.php file containing
function quizaccess_mypluginname_cron() {};
+
+=== 2.4 ===
+
+* Replaced time_left() with new time_left_display() and end_time() functions.
View
47 mod/quiz/attemptlib.php
@@ -1002,13 +1002,14 @@ public function get_question_action_time($slot) {
* @return int|false the number of seconds remaining for this attempt.
* False if there is no limit.
*/
- public function get_time_left($timenow) {
+ public function get_time_left_display($timenow) {
if ($this->attempt->state != self::IN_PROGRESS) {
return false;
}
- return $this->get_access_manager($timenow)->get_time_left($this->attempt, $timenow);
+ return $this->get_access_manager($timenow)->get_time_left_display($this->attempt, $timenow);
}
+
/**
* @return int the time when this attempt was submitted. 0 if it has not been
* submitted yet.
@@ -1269,30 +1270,39 @@ public function links_to_other_attempts(moodle_url $url) {
/**
* Check this attempt, to see if there are any state transitions that should
- * happen automatically.
+ * happen automatically. This function will update the attempt checkstatetime.
* @param int $timestamp the timestamp that should be stored as the modifed
* @param bool $studentisonline is the student currently interacting with Moodle?
*/
public function handle_if_time_expired($timestamp, $studentisonline) {
global $DB;
- $timeleft = $this->get_access_manager($timestamp)->get_time_left($this->attempt, $timestamp);
+ $timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt);
- if ($timeleft === false || $timeleft > 0) {
+ if ($timeclose === false || $this->is_preview()) {
+ $this->update_timecheckstate(null);
+ return; // No time limit
+ }
+ if ($timestamp < $timeclose) {
+ $this->update_timecheckstate($timeclose);
return; // Time has not yet expired.
}
// If the attempt is already overdue, look to see if it should be abandoned ...
if ($this->attempt->state == self::OVERDUE) {
- $timeoverdue = -$timeleft;
- if ($timeoverdue > $this->quizobj->get_quiz()->graceperiod) {
+ $timeoverdue = $timestamp - $timeclose;
+ $graceperiod = $this->quizobj->get_quiz()->graceperiod;
+ if ($timeoverdue >= $graceperiod) {
$this->process_abandon($timestamp, $studentisonline);
+ } else {
+ // Overdue time has not yet expired
+ $this->update_timecheckstate($timeclose + $graceperiod);
}
-
return; // ... and we are done.
}
if ($this->attempt->state != self::IN_PROGRESS) {
+ $this->update_timecheckstate(null);
return; // Attempt is already in a final state.
}
@@ -1311,6 +1321,10 @@ public function handle_if_time_expired($timestamp, $studentisonline) {
$this->process_abandon($timestamp, $studentisonline);
return;
}
+
+ // This is an overdue attempt with no overdue handling defined, so just abandon.
+ $this->process_abandon($timestamp, $studentisonline);
+ return;
}
/**
@@ -1373,6 +1387,7 @@ public function process_finish($timestamp, $processsubmitted) {
$this->attempt->timefinish = $timestamp;
$this->attempt->sumgrades = $this->quba->get_total_mark();
$this->attempt->state = self::FINISHED;
+ $this->attempt->timecheckstate = null;
$DB->update_record('quiz_attempts', $this->attempt);
if (!$this->is_preview()) {
@@ -1389,6 +1404,18 @@ public function process_finish($timestamp, $processsubmitted) {
}
/**
+ * Update this attempt timecheckstate if necessary.
+ * @param int|null the timecheckstate
+ */
+ public function update_timecheckstate($time) {
+ global $DB;
+ if ($this->attempt->timecheckstate !== $time) {
+ $this->attempt->timecheckstate = $time;
+ $DB->set_field('quiz_attempts', 'timecheckstate', $time, array('id'=>$this->attempt->id));
+ }
+ }
+
+ /**
* Mark this attempt as now overdue.
* @param int $timestamp the time to deem as now.
* @param bool $studentisonline is the student currently interacting with Moodle?
@@ -1399,6 +1426,9 @@ public function process_going_overdue($timestamp, $studentisonline) {
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::OVERDUE;
+ // If we knew the attempt close time, we could compute when the graceperiod ends.
+ // Instead we'll just fix it up through cron.
+ $this->attempt->timecheckstate = $timestamp;
$DB->update_record('quiz_attempts', $this->attempt);
$this->fire_state_transition_event('quiz_attempt_overdue', $timestamp);
@@ -1417,6 +1447,7 @@ public function process_abandon($timestamp, $studentisonline) {
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::ABANDONED;
+ $this->attempt->timecheckstate = null;
$DB->update_record('quiz_attempts', $this->attempt);
$this->fire_state_transition_event('quiz_attempt_abandoned', $timestamp);
View
2  mod/quiz/backup/moodle2/backup_quiz_stepslib.php
@@ -79,7 +79,7 @@ protected function define_structure() {
$attempt = new backup_nested_element('attempt', array('id'), array(
'userid', 'attemptnum', 'uniqueid', 'layout', 'currentpage', 'preview',
- 'state', 'timestart', 'timefinish', 'timemodified', 'sumgrades'));
+ 'state', 'timestart', 'timefinish', 'timemodified', 'timecheckstate', 'sumgrades'));
// This module is using questions, so produce the related question states and sessions
// attaching them to the $attempt element based in 'uniqueid' matching.
View
1  mod/quiz/backup/moodle2/restore_quiz_stepslib.php
@@ -306,6 +306,7 @@ protected function process_quiz_attempt($data) {
$data->timestart = $this->apply_date_offset($data->timestart);
$data->timefinish = $this->apply_date_offset($data->timefinish);
$data->timemodified = $this->apply_date_offset($data->timemodified);
+ $data->timecheckstate = $this->apply_date_offset($data->timecheckstate);
// Deals with up-grading pre-2.3 back-ups to 2.3+.
if (!isset($data->state)) {
View
76 mod/quiz/cronlib.php
@@ -40,15 +40,13 @@ class mod_quiz_overdue_attempt_updater {
/**
* Do the processing required.
* @param int $timenow the time to consider as 'now' during the processing.
- * @param int $processfrom the value of $processupto the last time update_overdue_attempts was
- * called called and completed successfully.
- * @param int $processto only process attempt modifed longer ago than this.
+ * @param int $processto only process attempt with timecheckstate longer ago than this.
* @return array with two elements, the number of attempt considered, and how many different quizzes that was.
*/
- public function update_overdue_attempts($timenow, $processfrom, $processto) {
+ public function update_overdue_attempts($timenow, $processto) {
global $DB;
- $attemptstoprocess = $this->get_list_of_overdue_attempts($processfrom, $processto);
+ $attemptstoprocess = $this->get_list_of_overdue_attempts($processto);
$course = null;
$quiz = null;
@@ -97,61 +95,27 @@ public function update_overdue_attempts($timenow, $processfrom, $processto) {
* @return moodle_recordset of quiz_attempts that need to be processed because time has
* passed. The array is sorted by courseid then quizid.
*/
- protected function get_list_of_overdue_attempts($processfrom, $processto) {
+ public function get_list_of_overdue_attempts($processto) {
global $DB;
+
+ // SQL to compute timeclose and timelimit for each attempt:
+ $quizausersql = quiz_get_attempt_usertime_sql();
+
// This query should have all the quiz_attempts columns.
return $DB->get_recordset_sql("
SELECT quiza.*,
- group_by_results.usertimeclose,
- group_by_results.usertimelimit
-
- FROM (
-
- SELECT iquiza.id AS attemptid,
- quiz.course,
- quiz.graceperiod,
- COALESCE(quo.timeclose, MAX(qgo.timeclose), quiz.timeclose) AS usertimeclose,
- COALESCE(quo.timelimit, MAX(qgo.timelimit), quiz.timelimit) AS usertimelimit
-
- FROM {quiz_attempts} iquiza
- JOIN {quiz} quiz ON quiz.id = iquiza.quiz
- LEFT JOIN {quiz_overrides} quo ON quo.quiz = quiz.id AND quo.userid = iquiza.userid
- LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
- LEFT JOIN {quiz_overrides} qgo ON qgo.quiz = quiz.id AND qgo.groupid = gm.groupid
-
- WHERE iquiza.state IN ('inprogress', 'overdue')
- AND iquiza.timemodified >= :processfrom
- AND iquiza.timemodified < :processto
-
- GROUP BY iquiza.id,
- quiz.course,
- quiz.timeclose,
- quiz.timelimit,
- quiz.graceperiod,
- quo.timeclose,
- quo.timelimit
- ) group_by_results
- JOIN {quiz_attempts} quiza ON quiza.id = group_by_results.attemptid
-
- WHERE (
- state = 'inprogress' AND (
- (usertimeclose > 0 AND :timenow1 > usertimeclose) OR
- (usertimelimit > 0 AND :timenow2 > quiza.timestart + usertimelimit)
- )
- )
- OR
- (
- state = 'overdue' AND (
- (usertimeclose > 0 AND :timenow3 > graceperiod + usertimeclose) OR
- (usertimelimit > 0 AND :timenow4 > graceperiod + quiza.timestart + usertimelimit)
- )
- )
-
- ORDER BY course, quiz",
-
- array('processfrom' => $processfrom, 'processto' => $processto,
- 'timenow1' => $processto, 'timenow2' => $processto,
- 'timenow3' => $processto, 'timenow4' => $processto));
+ quizauser.usertimeclose,
+ quizauser.usertimelimit
+
+ FROM {quiz_attempts} quiza
+ JOIN {quiz} quiz ON quiz.id = quiza.quiz
+ JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
+
+ WHERE quiza.state IN ('inprogress', 'overdue')
+ AND quiza.timecheckstate <= :processto
+ ORDER BY quiz.course, quiza.quiz",
+
+ array('processto' => $processto));
}
}
View
23 mod/quiz/db/events.php
@@ -43,6 +43,29 @@
'handlerfunction' => 'quiz_attempt_overdue_handler',
'schedule' => 'cron',
),
+
+ // Handle group events, so that open quiz attempts with group overrides get
+ // updated check times.
+ 'groups_member_added' => array (
+ 'handlerfile' => '/mod/quiz/locallib.php',
+ 'handlerfunction' => 'quiz_groups_member_added_handler',
+ 'schedule' => 'instant',
+ ),
+ 'groups_member_removed' => array (
+ 'handlerfile' => '/mod/quiz/locallib.php',
+ 'handlerfunction' => 'quiz_groups_member_removed_handler',
+ 'schedule' => 'instant',
+ ),
+ 'groups_members_removed' => array (
+ 'handlerfile' => '/mod/quiz/locallib.php',
+ 'handlerfunction' => 'quiz_groups_members_removed_handler',
+ 'schedule' => 'instant',
+ ),
+ 'groups_group_deleted' => array (
+ 'handlerfile' => '/mod/quiz/locallib.php',
+ 'handlerfunction' => 'quiz_groups_group_deleted_handler',
+ 'schedule' => 'instant',
+ ),
);
/* List of events generated by the quiz module, with the fields on the event object.
View
18 mod/quiz/db/install.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/quiz/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/quiz"
+<XMLDB PATH="mod/quiz/db" VERSION="20121006" COMMENT="XMLDB file for Moodle mod/quiz"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -9,7 +9,7 @@
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Standard Moodle primary key." NEXT="course"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the course this quiz is part of." PREVIOUS="id" NEXT="name"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz name." PREVIOUS="course" NEXT="intro"/>
- <FIELD NAME="intro" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz introduction text." PREVIOUS="name" NEXT="introformat"/>
+ <FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Quiz introduction text." PREVIOUS="name" NEXT="introformat"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Quiz intro text format." PREVIOUS="intro" NEXT="timeopen"/>
<FIELD NAME="timeopen" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when this quiz opens. (0 = no restriction.)" PREVIOUS="introformat" NEXT="timeclose"/>
<FIELD NAME="timeclose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when this quiz closes. (0 = no restriction.)" PREVIOUS="timeopen" NEXT="timelimit"/>
@@ -33,7 +33,7 @@
<FIELD NAME="navmethod" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="free" SEQUENCE="false" COMMENT="Any constraints on how the user is allowed to navigate around the quiz. Currently recognised values are 'free' and 'seq'." PREVIOUS="questionsperpage" NEXT="shufflequestions"/>
<FIELD NAME="shufflequestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the question order should be shuffled for each attempt." PREVIOUS="navmethod" NEXT="shuffleanswers"/>
<FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the parts of the question should be shuffled, in those question types that support it." PREVIOUS="shufflequestions" NEXT="questions"/>
- <FIELD NAME="questions" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated list of question ids, with 0s for page breaks. The quiz layout. See also the quiz_question_instances table." PREVIOUS="shuffleanswers" NEXT="sumgrades"/>
+ <FIELD NAME="questions" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated list of question ids, with 0s for page breaks. The quiz layout. See also the quiz_question_instances table." PREVIOUS="shuffleanswers" NEXT="sumgrades"/>
<FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total of all the question instance maxmarks." PREVIOUS="questions" NEXT="grade"/>
<FIELD NAME="grade" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total that the quiz overall grade is scaled to be out of." PREVIOUS="sumgrades" NEXT="timecreated"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when the quiz was added to the course." PREVIOUS="grade" NEXT="timemodified"/>
@@ -60,14 +60,15 @@
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the user whose attempt this is." PREVIOUS="quiz" NEXT="attempt"/>
<FIELD NAME="attempt" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Sequentially numbers this student's attempts at this quiz." PREVIOUS="userid" NEXT="uniqueid"/>
<FIELD NAME="uniqueid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key reference to the question_usage that holds the details of the the question_attempts that make up this quiz attempt." PREVIOUS="attempt" NEXT="layout"/>
- <FIELD NAME="layout" TYPE="text" LENGTH="small" NOTNULL="true" SEQUENCE="false" PREVIOUS="uniqueid" NEXT="currentpage"/>
+ <FIELD NAME="layout" TYPE="text" NOTNULL="true" SEQUENCE="false" PREVIOUS="uniqueid" NEXT="currentpage"/>
<FIELD NAME="currentpage" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="layout" NEXT="preview"/>
<FIELD NAME="preview" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="currentpage" NEXT="state"/>
<FIELD NAME="state" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="inprogress" SEQUENCE="false" COMMENT="The current state of the attempts. 'inprogress', 'overdue', 'finished' or 'abandoned'." PREVIOUS="preview" NEXT="timestart"/>
<FIELD NAME="timestart" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time when the attempt was started." PREVIOUS="state" NEXT="timefinish"/>
<FIELD NAME="timefinish" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time when the attempt was submitted. 0 if the attempt has not been submitted yet." PREVIOUS="timestart" NEXT="timemodified"/>
- <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time." PREVIOUS="timefinish" NEXT="sumgrades"/>
- <FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Total marks for this attempt." PREVIOUS="timemodified" NEXT="needsupgradetonewqe"/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Last modified time." PREVIOUS="timefinish" NEXT="timecheckstate"/>
+ <FIELD NAME="timecheckstate" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Next time quiz cron should check attempt for state changes. NULL means never check." PREVIOUS="timemodified" NEXT="sumgrades"/>
+ <FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Total marks for this attempt." PREVIOUS="timecheckstate" NEXT="needsupgradetonewqe"/>
<FIELD NAME="needsupgradetonewqe" TYPE="int" LENGTH="3" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used during the upgrade from Moodle 2.0 to 2.1. This will be removed in the future." PREVIOUS="sumgrades"/>
</FIELDS>
<KEYS>
@@ -77,7 +78,8 @@
<KEY NAME="uniqueid" TYPE="foreign-unique" FIELDS="uniqueid" REFTABLE="question_usages" REFFIELDS="id" PREVIOUS="userid"/>
</KEYS>
<INDEXES>
- <INDEX NAME="quiz-userid-attempt" UNIQUE="true" FIELDS="quiz, userid, attempt"/>
+ <INDEX NAME="quiz-userid-attempt" UNIQUE="true" FIELDS="quiz, userid, attempt" NEXT="state-timecheckstate"/>
+ <INDEX NAME="state-timecheckstate" UNIQUE="false" FIELDS="state, timecheckstate" PREVIOUS="quiz-userid-attempt"/>
</INDEXES>
</TABLE>
<TABLE NAME="quiz_grades" COMMENT="Stores the overall grade for each user on the quiz, based on their various attempts and the quiz.grademethod setting." PREVIOUS="quiz_attempts" NEXT="quiz_question_instances">
@@ -157,4 +159,4 @@
</INDEXES>
</TABLE>
</TABLES>
-</XMLDB>
+</XMLDB>
View
32 mod/quiz/db/upgrade.php
@@ -360,6 +360,38 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2012061703, 'quiz');
}
+ if ($oldversion < 2012100801) {
+
+ // Define field timecheckstate to be added to quiz_attempts
+ $table = new xmldb_table('quiz_attempts');
+ $field = new xmldb_field('timecheckstate', XMLDB_TYPE_INTEGER, '10', null, null, null, '0', 'timemodified');
+
+ // Conditionally launch add field timecheckstate
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Define index state-timecheckstate (not unique) to be added to quiz_attempts
+ $table = new xmldb_table('quiz_attempts');
+ $index = new xmldb_index('state-timecheckstate', XMLDB_INDEX_NOTUNIQUE, array('state', 'timecheckstate'));
+
+ // Conditionally launch add index state-timecheckstate
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Overdue cron no longer needs these
+ unset_config('overduelastrun', 'quiz');
+ unset_config('overduedoneto', 'quiz');
+
+ // Update timecheckstate on all open attempts
+ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+ quiz_update_open_attempts(array());
+
+ // quiz savepoint reached
+ upgrade_mod_savepoint(true, 2012100801, 'quiz');
+ }
+
return true;
}
View
52 mod/quiz/lib.php
@@ -138,6 +138,14 @@ function quiz_update_instance($quiz, $mform) {
quiz_update_grades($quiz);
}
+ $updateattempts = $oldquiz->timelimit != $quiz->timelimit
+ || $oldquiz->timeclose != $quiz->timeclose
+ || $oldquiz->graceperiod != $quiz->graceperiod;
+ if ($updateattempts) {
+ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+ quiz_update_open_attempts(array('quizid'=>$quiz->id));
+ }
+
// Delete any previous preview attempts.
quiz_delete_previews($quiz);
@@ -284,13 +292,25 @@ function quiz_update_effective_access($quiz, $userid) {
$override->timeopen = min($opens);
}
if (is_null($override->timeclose) && count($closes)) {
- $override->timeclose = max($closes);
+ if (in_array(0, $closes)) {
+ $override->timeclose = 0;
+ } else {
+ $override->timeclose = max($closes);
+ }
}
if (is_null($override->timelimit) && count($limits)) {
- $override->timelimit = max($limits);
+ if (in_array(0, $limits)) {
+ $override->timelimit = 0;
+ } else {
+ $override->timelimit = max($limits);
+ }
}
if (is_null($override->attempts) && count($attempts)) {
- $override->attempts = max($attempts);
+ if (in_array(0, $attempts)) {
+ $override->attempts = 0;
+ } else {
+ $override->attempts = max($attempts);
+ }
}
if (is_null($override->password) && count($passwords)) {
$override->password = array_shift($passwords);
@@ -446,32 +466,20 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
*/
function quiz_cron() {
global $CFG;
- mtrace('');
- // Since the quiz specifies $module->cron = 60, so that the subplugins can
- // have frequent cron if they need it, we now need to do our own scheduling.
- $quizconfig = get_config('quiz');
- if (!isset($quizconfig->overduelastrun)) {
- $quizconfig->overduelastrun = 0;
- $quizconfig->overduedoneto = 0;
- }
+ require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
+ mtrace('');
$timenow = time();
- if ($timenow > $quizconfig->overduelastrun + 3600) {
- require_once($CFG->dirroot . '/mod/quiz/cronlib.php');
- $overduehander = new mod_quiz_overdue_attempt_updater();
+ $overduehander = new mod_quiz_overdue_attempt_updater();
- $processto = $timenow - $quizconfig->graceperiodmin;
+ $processto = $timenow - get_config('quiz', 'graceperiodmin');
- mtrace(' Looking for quiz overdue quiz attempts between ' .
- userdate($quizconfig->overduedoneto) . ' and ' . userdate($processto) . '...');
+ mtrace(' Looking for quiz overdue quiz attempts...');
- list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
- set_config('overduelastrun', $timenow, 'quiz');
- set_config('overduedoneto', $processto, 'quiz');
+ list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $processto);
- mtrace(' Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
- }
+ mtrace(' Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
// Run cron for our sub-plugin types.
cron_execute_plugin_type('quiz', 'quiz reports');
View
200 mod/quiz/locallib.php
@@ -65,7 +65,7 @@
* user starting at the current time. The ->id field is not set. The object is
* NOT written to the database.
*
- * @param object $quiz the quiz to create an attempt for.
+ * @param object $quizobj the quiz object to create an attempt for.
* @param int $attemptnumber the sequence number for the attempt.
* @param object $lastattempt the previous attempt by this user, if any. Only needed
* if $attemptnumber > 1 and $quiz->attemptonlast is true.
@@ -74,9 +74,10 @@
*
* @return object the newly created attempt object.
*/
-function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
+function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
global $USER;
+ $quiz = $quizobj->get_quiz();
if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) {
throw new moodle_exception('cannotstartgradesmismatch', 'quiz',
new moodle_url('/mod/quiz/view.php', array('q' => $quiz->id)),
@@ -112,6 +113,13 @@ function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $isp
$attempt->preview = 1;
}
+ $timeclose = $quizobj->get_access_manager($timenow)->get_end_time($attempt);
+ if ($timeclose === false || $ispreview) {
+ $attempt->timecheckstate = null;
+ } else {
+ $attempt->timecheckstate = $timeclose;
+ }
+
return $attempt;
}
@@ -755,6 +763,142 @@ function quiz_update_all_final_grades($quiz) {
}
/**
+ * Efficiently update check state time on all open attempts
+ *
+ * @param array $conditions optional restrictions on which attempts to update
+ * Allowed conditions:
+ * courseid => (array|int) attempts in given course(s)
+ * userid => (array|int) attempts for given user(s)
+ * quizid => (array|int) attempts in given quiz(s)
+ * groupid => (array|int) quizzes with some override for given group(s)
+ *
+ */
+function quiz_update_open_attempts(array $conditions) {
+ global $DB;
+
+ foreach ($conditions as &$value) {
+ if (!is_array($value)) {
+ $value = array($value);
+ }
+ }
+
+ $params = array();
+ $coursecond = '';
+ $usercond = '';
+ $quizcond = '';
+ $groupcond = '';
+
+ if (isset($conditions['courseid'])) {
+ list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid');
+ $params = array_merge($params, $inparams);
+ $coursecond = "AND quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
+ }
+ if (isset($conditions['userid'])) {
+ list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid');
+ $params = array_merge($params, $inparams);
+ $usercond = "AND quiza.userid $incond";
+ }
+ if (isset($conditions['quizid'])) {
+ list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid');
+ $params = array_merge($params, $inparams);
+ $quizcond = "AND quiza.quiz $incond";
+ }
+ if (isset($conditions['groupid'])) {
+ list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid');
+ $params = array_merge($params, $inparams);
+ $groupcond = "AND quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
+ }
+
+ // SQL to compute timeclose and timelimit for each attempt:
+ $quizausersql = quiz_get_attempt_usertime_sql();
+
+ // SQL to compute the new timecheckstate
+ $timecheckstatesql = "
+ CASE WHEN quizauser.usertimelimit = 0 AND quizauser.usertimeclose = 0 THEN NULL
+ WHEN quizauser.usertimelimit = 0 THEN quizauser.usertimeclose
+ WHEN quizauser.usertimeclose = 0 THEN quiza.timestart + quizauser.usertimelimit
+ WHEN quiza.timestart + quizauser.usertimelimit < quizauser.usertimeclose THEN quiza.timestart + quizauser.usertimelimit
+ ELSE quizauser.usertimeclose END +
+ CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END";
+
+ // SQL to select which attempts to process
+ $attemptselect = " quiza.state IN ('inprogress', 'overdue')
+ $coursecond
+ $usercond
+ $quizcond
+ $groupcond";
+
+ /*
+ * Each database handles updates with inner joins differently:
+ * - mysql does not allow a FROM clause
+ * - postgres and mssql allow FROM but handle table aliases differently
+ * - oracle requires a subquery
+ *
+ * Different code for each database.
+ */
+
+ $dbfamily = $DB->get_dbfamily();
+ if ($dbfamily == 'mysql') {
+ $updatesql = "UPDATE {quiz_attempts} quiza
+ JOIN {quiz} quiz ON quiz.id = quiza.quiz
+ JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
+ SET quiza.timecheckstate = $timecheckstatesql
+ WHERE $attemptselect";
+ } else if ($dbfamily == 'postgres') {
+ $updatesql = "UPDATE {quiz_attempts} quiza
+ SET timecheckstate = $timecheckstatesql
+ FROM {quiz} quiz, ( $quizausersql ) quizauser
+ WHERE quiz.id = quiza.quiz
+ AND quizauser.id = quiza.id
+ AND $attemptselect";
+ } else if ($dbfamily == 'mssql') {
+ $updatesql = "UPDATE quiza
+ SET timecheckstate = $timecheckstatesql
+ FROM {quiz_attempts} quiza
+ JOIN {quiz} quiz ON quiz.id = quiza.quiz
+ JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
+ WHERE $attemptselect";
+ } else {
+ // oracle, sqlite and others
+ $updatesql = "UPDATE {quiz_attempts} quiza
+ SET timecheckstate = (
+ SELECT $timecheckstatesql
+ FROM {quiz} quiz, ( $quizausersql ) quizauser
+ WHERE quiz.id = quiza.quiz
+ AND quizauser.id = quiza.id
+ )
+ WHERE $attemptselect";
+ }
+
+ $DB->execute($updatesql, $params);
+}
+
+/**
+ * Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides.
+ *
+ * @return string SQL select with columns attempt.id, usertimeclose, usertimelimit
+ */
+function quiz_get_attempt_usertime_sql() {
+ // The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede
+ // any other group override
+ $quizausersql = "
+ SELECT iquiza.id,
+ COALESCE(MAX(quo.timeclose), MAX(qgo1.timeclose), MAX(qgo2.timeclose), iquiz.timeclose) AS usertimeclose,
+ COALESCE(MAX(quo.timelimit), MAX(qgo3.timelimit), MAX(qgo4.timelimit), iquiz.timelimit) AS usertimelimit
+
+ FROM {quiz_attempts} iquiza
+ JOIN {quiz} iquiz ON iquiz.id = iquiza.quiz
+ LEFT JOIN {quiz_overrides} quo ON quo.quiz = iquiza.quiz AND quo.userid = iquiza.userid
+ LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
+ LEFT JOIN {quiz_overrides} qgo1 ON qgo1.quiz = iquiza.quiz AND qgo1.groupid = gm.groupid AND qgo1.timeclose = 0
+ LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0
+ LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0
+ LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0
+ GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit";
+ return $quizausersql;
+}
+
+/**
* Return the attempt with the best grade for a quiz
*
* Which attempt is the best depends on $quiz->grademethod. If the grade
@@ -1446,6 +1590,58 @@ function quiz_attempt_overdue_handler($event) {
}
/**
+ * Handle groups_member_added event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_member_added_handler($event) {
+ quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid));
+}
+
+/**
+ * Handle groups_member_removed event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_member_removed_handler($event) {
+ quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid));
+}
+
+/**
+ * Handle groups_group_deleted event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_group_deleted_handler($event) {
+ global $DB;
+
+ // It would be nice if we got the groupid that was deleted.
+ // Instead, we just update all quizzes with orphaned group overrides
+ $sql = "SELECT o.id, o.quiz
+ FROM {quiz_overrides} o
+ JOIN {quiz} quiz ON quiz.id = o.quiz
+ LEFT JOIN {groups} grp ON grp.id = o.groupid
+ WHERE quiz.course = :courseid AND grp.id IS NULL";
+ $params = array('courseid'=>$event->courseid);
+ $records = $DB->get_records_sql_menu($sql, $params);
+ $DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
+ quiz_update_open_attempts(array('quizid'=>array_unique(array_values($records))));
+}
+
+/**
+ * Handle groups_members_removed event
+ *
+ * @param object $event the event object.
+ */
+function quiz_groups_members_removed_handler($event) {
+ if ($event->userid == 0) {
+ quiz_update_open_attempts(array('courseid'=>$event->courseid));
+ } else {
+ quiz_update_open_attempts(array('courseid'=>$event->courseid, 'userid'=>$event->userid));
+ }
+}
+
+/**
* Get the information about the standard quiz JavaScript module.
* @return array a standard jsmodule structure.
*/
View
17 mod/quiz/module.js
@@ -50,6 +50,9 @@ M.mod_quiz.timer = {
// Timestamp at which time runs out, according to the student's computer's clock.
endtime: 0,
+
+ // Is this a quiz preview?
+ preview: 0,
// This records the id of the timeout that updates the clock periodically,
// so we can cancel.
@@ -57,11 +60,13 @@ M.mod_quiz.timer = {
/**
* @param Y the YUI object
- * @param timeleft, the time remaining, in seconds.
+ * @param start, the timer starting time, in seconds.
+ * @param preview, is this a quiz preview?
*/
- init: function(Y, timeleft) {
+ init: function(Y, start, preview) {
M.mod_quiz.timer.Y = Y;
- M.mod_quiz.timer.endtime = new Date().getTime() + timeleft*1000;
+ M.mod_quiz.timer.endtime = new Date().getTime() + start*1000;
+ M.mod_quiz.timer.preview = preview;
M.mod_quiz.timer.update();
Y.one('#quiz-timer').setStyle('display', 'block');
},
@@ -90,6 +95,12 @@ M.mod_quiz.timer = {
update: function() {
var Y = M.mod_quiz.timer.Y;
var secondsleft = Math.floor((M.mod_quiz.timer.endtime - new Date().getTime())/1000);
+
+ // If this is a preview and time expired, display timeleft 0 and don't renew the timer.
+ if (M.mod_quiz.timer.preview && secondsleft < 0) {
+ Y.one('#quiz-time-left').setContent('0:00:00');
+ return;
+ }
// If time has expired, Set the hidden form field that says time has expired.
if (secondsleft < 0) {
View
1  mod/quiz/overrideedit.php
@@ -169,6 +169,7 @@
$fromform->id = $DB->insert_record('quiz_overrides', $fromform);
}
+ quiz_update_open_attempts(array('quizid'=>$quiz->id));
quiz_update_events($quiz, $fromform);
add_to_log($cm->course, 'quiz', 'edit override',
View
13 mod/quiz/processattempt.php
@@ -67,12 +67,17 @@
// to show the student another page of the quiz. Just finish now.
$graceperiodmin = null;
$accessmanager = $attemptobj->get_access_manager($timenow);
-$timeleft = $accessmanager->get_time_left($attemptobj->get_attempt(), $timenow);
+$timeclose = $accessmanager->get_end_time($attemptobj->get_attempt());
+
+// Don't enforce timeclose for previews
+if ($attemptobj->is_preview()) {
+ $timeclose = false;
+}
$toolate = false;
-if ($timeleft !== false && $timeleft < QUIZ_MIN_TIME_TO_CONTINUE) {
+if ($timeclose !== false && $timenow > $timeclose - QUIZ_MIN_TIME_TO_CONTINUE) {
$timeup = true;
$graceperiodmin = get_config('quiz', 'graceperiodmin');
- if ($timeleft < -$graceperiodmin) {
+ if ($timenow > $timeclose + $graceperiodmin) {
$toolate = true;
}
}
@@ -105,7 +110,7 @@
if (is_null($graceperiodmin)) {
$graceperiodmin = get_config('quiz', 'graceperiodmin');
}
- if ($timeleft < -$attemptobj->get_quiz()->graceperiod - $graceperiodmin) {
+ if ($timenow > $timeclose + $attemptobj->get_quiz()->graceperiod + $graceperiodmin) {
// Grace period has run out.
$finishattempt = true;
$becomingabandoned = true;
View
20 mod/quiz/renderer.php
@@ -265,12 +265,16 @@ public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpag
*/
public function countdown_timer(quiz_attempt $attemptobj, $timenow) {
- $timeleft = $attemptobj->get_time_left($timenow);
+ $timeleft = $attemptobj->get_time_left_display($timenow);
if ($timeleft !== false) {
- // Make sure the timer starts just above zero. If $timeleft was <= 0, then
- // this will just have the effect of causing the quiz to be submitted immediately.
- $timerstartvalue = max($timeleft, 1);
- $this->initialise_timer($timerstartvalue);
+ $ispreview = $attemptobj->is_preview();
+ $timerstartvalue = $timeleft;
+ if (!$ispreview) {
+ // Make sure the timer starts just above zero. If $timeleft was <= 0, then
+ // this will just have the effect of causing the quiz to be submitted immediately.
+ $timerstartvalue = max($timerstartvalue, 1);
+ }
+ $this->initialise_timer($timerstartvalue, $ispreview);
}
return html_writer::tag('div', get_string('timeleft', 'quiz') . ' ' .
@@ -486,9 +490,9 @@ public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
* Output the JavaScript required to initialise the countdown timer.
* @param int $timerstartvalue time remaining, in seconds.
*/
- public function initialise_timer($timerstartvalue) {
- $this->page->requires->js_init_call('M.mod_quiz.timer.init',
- array($timerstartvalue), false, quiz_get_js_module());
+ public function initialise_timer($timerstartvalue, $ispreview) {
+ $options = array($timerstartvalue, (bool)$ispreview);
+ $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module());
}
/**
View
2  mod/quiz/startattempt.php
@@ -165,7 +165,7 @@
// Create the new attempt and initialize the question sessions
$timenow = time(); // Update time now, in case the server is running really slowly.
-$attempt = quiz_create_attempt($quizobj->get_quiz(), $attemptnumber, $lastattempt, $timenow,
+$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow,
$quizobj->is_preview_user());
if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) {
View
388 mod/quiz/tests/attempts_test.php
@@ -0,0 +1,388 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Quiz attempt overdue handling tests
+ *
+ * @package mod_quiz
+ * @category phpunit
+ * @copyright 2012 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/group/lib.php');
+
+/**
+ * Unit tests for quiz attempt overdue handling
+ *
+ * @package mod_quiz
+ * @category phpunit
+ * @copyright 2012 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_quiz_attempt_overdue_testcase extends advanced_testcase {
+ /**
+ * Test the functions quiz_update_open_attempts() and get_list_of_overdue_attempts()
+ */
+ public function test_bulk_update_functions() {
+ global $DB,$CFG;
+
+ require_once($CFG->dirroot.'/mod/quiz/cronlib.php');
+
+ $this->resetAfterTest();
+
+ $this->setAdminUser();
+
+ // Setup course, user and groups
+
+ $course = $this->getDataGenerator()->create_course();
+ $user1 = $this->getDataGenerator()->create_user();
+ $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+ $this->assertNotEmpty($studentrole);
+ $this->assertTrue(enrol_try_internal_enrol($course->id, $user1->id, $studentrole->id));
+ $group1 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+ $group2 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+ $group3 = $this->getDataGenerator()->create_group(array('courseid'=>$course->id));
+ $this->assertTrue(groups_add_member($group1, $user1));
+ $this->assertTrue(groups_add_member($group2, $user1));
+
+ $uniqueid = 0;
+ $usertimes = array();
+
+ $quiz_generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
+
+ // Basic quiz settings
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, 'message'=>'Test1A');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>1800));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>1800, 'message'=>'Test1B');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>0, 'message'=>'Test1C');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>600, 'message'=>'Test1D');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>0));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>0, 'message'=>'Test1E');
+
+ // Group overrides
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>null));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>0, 'message'=>'Test2A');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1100, 'timelimit'=>null));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1100, 'timelimit'=>0, 'message'=>'Test2B');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>700));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>700, 'message'=>'Test2C');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>500));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>500, 'message'=>'Test2D');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>0));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>0, '', 'message'=>'Test2E');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>500, '', 'message'=>'Test2F');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>500, '', 'message'=>'Test2G');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group3->id, 'timeclose'=>1300, 'timelimit'=>500)); // user not in group
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, '', 'message'=>'Test2H');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, '', 'message'=>'Test2I');
+
+ // Multiple group overrides
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>501));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1301, 'timelimit'=>500));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3A');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3B');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1301, 'timelimit'=>500));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1300, 'timelimit'=>501));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3C');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3D');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1301, 'timelimit'=>500));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1300, 'timelimit'=>501));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group3->id, 'timeclose'=>1500, 'timelimit'=>1000)); // user not in group
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3E');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>501, '', 'message'=>'Test3F');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>null, 'timelimit'=>501));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>501, '', 'message'=>'Test3G');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>501, '', 'message'=>'Test3H');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1301, 'timelimit'=>null));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>500, '', 'message'=>'Test3I');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>500, '', 'message'=>'Test3J');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1301, 'timelimit'=>0));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>0, '', 'message'=>'Test3K');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1301, 'timelimit'=>0, '', 'message'=>'Test3L');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>500));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>0, 'timelimit'=>501));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>501, '', 'message'=>'Test3M');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>501, '', 'message'=>'Test3N');
+
+ // User overrides
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>601));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>601, '', 'message'=>'Test4A');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>601, '', 'message'=>'Test4B');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>0, 'timelimit'=>601));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>601, '', 'message'=>'Test4C');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>601, '', 'message'=>'Test4D');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>0));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>0, '', 'message'=>'Test4E');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>0, '', 'message'=>'Test4F');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>null, 'timelimit'=>601));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>601, '', 'message'=>'Test4G');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>601, '', 'message'=>'Test4H');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>null, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>null, 'timelimit'=>601));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>601, '', 'message'=>'Test4I');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>601, '', 'message'=>'Test4J');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>null));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>700, '', 'message'=>'Test4K');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>700, '', 'message'=>'Test4L');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>null));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'timeclose'=>1201, 'timelimit'=>null));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>600, '', 'message'=>'Test4M');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1201, 'timelimit'=>600, '', 'message'=>'Test4N');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>700));
+ $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'userid'=>0, 'timeclose'=>1201, 'timelimit'=>601)); // not user
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>700, '', 'message'=>'Test4O');
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>1000, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++, 'attempt'=>1));
+ $usertimes[$attemptid] = array('timeclose'=>1300, 'timelimit'=>700, '', 'message'=>'Test4P');
+
+ // Attempt state overdue
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>600, 'overduehandling'=>'graceperiod', 'graceperiod'=>250));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'overdue', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>1200, 'timelimit'=>600, '', 'message'=>'Test5A');
+
+ $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>0, 'timelimit'=>600, 'overduehandling'=>'graceperiod', 'graceperiod'=>250));
+ $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'overdue', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++));
+ $usertimes[$attemptid] = array('timeclose'=>0, 'timelimit'=>600, '', 'message'=>'Test5B');
+
+ //
+ // Test quiz_update_open_attempts()
+ //
+
+ quiz_update_open_attempts(array('courseid'=>$course->id));
+ foreach ($usertimes as $attemptid=>$times) {