diff --git a/mod/quiz/accessmanager.php b/mod/quiz/accessmanager.php index 11efebd8c54..fe0f6ccde51 100644 --- a/mod/quiz/accessmanager.php +++ b/mod/quiz/accessmanager.php @@ -392,17 +392,35 @@ class quiz_access_manager { } /** - * 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; } diff --git a/mod/quiz/accessrule/accessrulebase.php b/mod/quiz/accessrule/accessrulebase.php index a3d9fa1b14f..4b585cd8af6 100644 --- a/mod/quiz/accessrule/accessrulebase.php +++ b/mod/quiz/accessrule/accessrulebase.php @@ -180,14 +180,28 @@ abstract class quiz_access_rule_base { /** * 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; } /** diff --git a/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php b/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php index 1dcc62f1ca2..dc27fcac804 100644 --- a/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php +++ b/mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php @@ -55,7 +55,8 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase { $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 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase { $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 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase { $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 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase { $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 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase { $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)); diff --git a/mod/quiz/accessrule/ipaddress/tests/rule_test.php b/mod/quiz/accessrule/ipaddress/tests/rule_test.php index 007000a32a4..3f8f520af1f 100644 --- a/mod/quiz/accessrule/ipaddress/tests/rule_test.php +++ b/mod/quiz/accessrule/ipaddress/tests/rule_test.php @@ -56,7 +56,8 @@ class quizaccess_ipaddress_testcase extends basic_testcase { $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 @@ class quizaccess_ipaddress_testcase extends basic_testcase { $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)); } } diff --git a/mod/quiz/accessrule/numattempts/tests/rule_test.php b/mod/quiz/accessrule/numattempts/tests/rule_test.php index e99b21963e3..5ab1985da64 100644 --- a/mod/quiz/accessrule/numattempts/tests/rule_test.php +++ b/mod/quiz/accessrule/numattempts/tests/rule_test.php @@ -64,6 +64,7 @@ class quizaccess_numattempts_testcase extends basic_testcase { $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)); } } diff --git a/mod/quiz/accessrule/openclosedate/rule.php b/mod/quiz/accessrule/openclosedate/rule.php index 2a33a1f1c8a..639bb1a4ea7 100644 --- a/mod/quiz/accessrule/openclosedate/rule.php +++ b/mod/quiz/accessrule/openclosedate/rule.php @@ -93,20 +93,24 @@ class quizaccess_openclosedate extends quiz_access_rule_base { 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; } diff --git a/mod/quiz/accessrule/openclosedate/tests/rule_test.php b/mod/quiz/accessrule/openclosedate/tests/rule_test.php index 0dea09c5e39..4d85d33fcba 100644 --- a/mod/quiz/accessrule/openclosedate/tests/rule_test.php +++ b/mod/quiz/accessrule/openclosedate/tests/rule_test.php @@ -55,15 +55,17 @@ class quizaccess_openclosedate_testcase extends basic_testcase { $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 @@ class quizaccess_openclosedate_testcase extends basic_testcase { 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 @@ class quizaccess_openclosedate_testcase extends basic_testcase { $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 @@ class quizaccess_openclosedate_testcase extends basic_testcase { $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 @@ class quizaccess_openclosedate_testcase extends basic_testcase { 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 @@ class quizaccess_openclosedate_testcase extends basic_testcase { $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() { diff --git a/mod/quiz/accessrule/password/tests/rule_test.php b/mod/quiz/accessrule/password/tests/rule_test.php index eac66b7136f..5fede6f2f84 100644 --- a/mod/quiz/accessrule/password/tests/rule_test.php +++ b/mod/quiz/accessrule/password/tests/rule_test.php @@ -53,6 +53,7 @@ class quizaccess_password_testcase extends basic_testcase { 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)); } } diff --git a/mod/quiz/accessrule/safebrowser/tests/rule_test.php b/mod/quiz/accessrule/safebrowser/tests/rule_test.php index 6922e999c98..470ecc9a8c9 100644 --- a/mod/quiz/accessrule/safebrowser/tests/rule_test.php +++ b/mod/quiz/accessrule/safebrowser/tests/rule_test.php @@ -58,6 +58,7 @@ class quizaccess_safebrowser_testcase extends basic_testcase { $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)); } } diff --git a/mod/quiz/accessrule/securewindow/tests/rule_test.php b/mod/quiz/accessrule/securewindow/tests/rule_test.php index 3bfe25d15f7..4ee3c7433de 100644 --- a/mod/quiz/accessrule/securewindow/tests/rule_test.php +++ b/mod/quiz/accessrule/securewindow/tests/rule_test.php @@ -54,6 +54,7 @@ class quizaccess_securewindow_testcase extends basic_testcase { $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)); } } diff --git a/mod/quiz/accessrule/timelimit/rule.php b/mod/quiz/accessrule/timelimit/rule.php index eff0dbbaf08..59558408be9 100644 --- a/mod/quiz/accessrule/timelimit/rule.php +++ b/mod/quiz/accessrule/timelimit/rule.php @@ -52,7 +52,17 @@ class quizaccess_timelimit extends quiz_access_rule_base { 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; + } } diff --git a/mod/quiz/accessrule/timelimit/tests/rule_test.php b/mod/quiz/accessrule/timelimit/tests/rule_test.php index 4175190293f..65de6926b93 100644 --- a/mod/quiz/accessrule/timelimit/tests/rule_test.php +++ b/mod/quiz/accessrule/timelimit/tests/rule_test.php @@ -51,9 +51,11 @@ class quizaccess_timelimit_testcase extends basic_testcase { 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)); diff --git a/mod/quiz/accessrule/upgrade.txt b/mod/quiz/accessrule/upgrade.txt index 12f117ba82f..a8d94ff3a01 100644 --- a/mod/quiz/accessrule/upgrade.txt +++ b/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. \ No newline at end of file diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 4be51af2633..bf5d5427a46 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -1002,13 +1002,14 @@ class quiz_attempt { * @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 @@ class quiz_attempt { /** * 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 @@ class quiz_attempt { $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 @@ class quiz_attempt { $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()) { @@ -1388,6 +1403,18 @@ class quiz_attempt { $transaction->allow_commit(); } + /** + * 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. @@ -1399,6 +1426,9 @@ class quiz_attempt { $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 @@ class quiz_attempt { $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); diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php index 80f3e6eb665..63a897a27c0 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php @@ -79,7 +79,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru $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. diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php index 58a38d17b06..8014bc1feb8 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -306,6 +306,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $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)) { diff --git a/mod/quiz/cronlib.php b/mod/quiz/cronlib.php index 9e4e6a4dc78..c8776de38e7 100644 --- a/mod/quiz/cronlib.php +++ b/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 @@ class mod_quiz_overdue_attempt_updater { * @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 + quizauser.usertimeclose, + quizauser.usertimelimit - FROM ( + FROM {quiz_attempts} quiza + JOIN {quiz} quiz ON quiz.id = quiza.quiz + JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id - 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 + WHERE quiza.state IN ('inprogress', 'overdue') + AND quiza.timecheckstate <= :processto + ORDER BY quiz.course, quiza.quiz", - 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)); + array('processto' => $processto)); } } diff --git a/mod/quiz/db/events.php b/mod/quiz/db/events.php index 0ff77f52866..f2deac10e73 100644 --- a/mod/quiz/db/events.php +++ b/mod/quiz/db/events.php @@ -43,6 +43,29 @@ $handlers = array( '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. diff --git a/mod/quiz/db/install.xml b/mod/quiz/db/install.xml index dede549feda..fa3186d0a97 100644 --- a/mod/quiz/db/install.xml +++ b/mod/quiz/db/install.xml @@ -1,5 +1,5 @@ - @@ -9,7 +9,7 @@ - + @@ -33,7 +33,7 @@ - + @@ -60,14 +60,15 @@ - + - - + + + @@ -77,7 +78,8 @@ - + + @@ -157,4 +159,4 @@
-
+ \ No newline at end of file diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php index cb8e76df0a9..b8bb7093c42 100644 --- a/mod/quiz/db/upgrade.php +++ b/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; } diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 3c55cca11a3..1aae44863c9 100644 --- a/mod/quiz/lib.php +++ b/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; + + require_once($CFG->dirroot . '/mod/quiz/cronlib.php'); 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; - } - $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'); diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 4f2c271b359..ea82013443f 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -65,7 +65,7 @@ define('QUIZ_MIN_TIME_TO_CONTINUE', '2'); * 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 @@ define('QUIZ_MIN_TIME_TO_CONTINUE', '2'); * * @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; } @@ -754,6 +762,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 * @@ -1445,6 +1589,58 @@ function quiz_attempt_overdue_handler($event) { context_module::instance($cm->id), $cm); } +/** + * 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. diff --git a/mod/quiz/module.js b/mod/quiz/module.js index ce0d3597a67..b70b9636a95 100644 --- a/mod/quiz/module.js +++ b/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) { diff --git a/mod/quiz/overrideedit.php b/mod/quiz/overrideedit.php index fa0c53d84f8..b298780312e 100644 --- a/mod/quiz/overrideedit.php +++ b/mod/quiz/overrideedit.php @@ -169,6 +169,7 @@ if ($mform->is_cancelled()) { $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', diff --git a/mod/quiz/processattempt.php b/mod/quiz/processattempt.php index aa4a171f35c..b66d2ffea23 100644 --- a/mod/quiz/processattempt.php +++ b/mod/quiz/processattempt.php @@ -67,12 +67,17 @@ if ($page == -1) { // 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 ($timeup) { 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; diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index d8fcae117c3..31da347d1f3 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -265,12 +265,16 @@ class mod_quiz_renderer extends plugin_renderer_base { */ 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 @@ class mod_quiz_renderer extends plugin_renderer_base { * 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()); } /** diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index 8cead805b61..374db3fc7f4 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -165,7 +165,7 @@ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // 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)) { diff --git a/mod/quiz/tests/attempts_test.php b/mod/quiz/tests/attempts_test.php new file mode 100644 index 00000000000..b70c26cffb7 --- /dev/null +++ b/mod/quiz/tests/attempts_test.php @@ -0,0 +1,388 @@ +. + +/** + * 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) { + $attempt = $DB->get_record('quiz_attempts', array('id'=>$attemptid)); + $this->assertTrue(false !== $attempt, $times['message']); + + if ($attempt->state == 'overdue') { + $graceperiod = $DB->get_field('quiz', 'graceperiod', array('id'=>$attempt->quiz)); + } else { + $graceperiod = 0; + } + if ($times['timeclose'] > 0 and $times['timelimit'] > 0) { + $this->assertEquals(min($times['timeclose'], $attempt->timestart + $times['timelimit']) + $graceperiod, $attempt->timecheckstate, $times['message']); + } else if ($times['timeclose'] > 0) { + $this->assertEquals($times['timeclose'] + $graceperiod, $attempt->timecheckstate <= $times['timeclose'], $times['message']); + } else if ($times['timelimit'] > 0) { + $this->assertEquals($attempt->timestart + $times['timelimit'] + $graceperiod, $attempt->timecheckstate, $times['message']); + } else { + $this->assertNull($attempt->timecheckstate, $times['message']); + } + } + + // + // Test get_list_of_overdue_attempts() + // + + $overduehander = new mod_quiz_overdue_attempt_updater(); + + $attempts = $overduehander->get_list_of_overdue_attempts(100000); // way in the future + $count = 0; + foreach ($attempts as $attempt) { + $this->assertTrue(isset($usertimes[$attempt->id])); + $times = $usertimes[$attempt->id]; + $this->assertEquals($times['timeclose'], $attempt->usertimeclose, $times['message']); + $this->assertEquals($times['timelimit'], $attempt->usertimelimit, $times['message']); + $count++; + + } + $this->assertEquals($DB->count_records_select('quiz_attempts', 'timecheckstate IS NOT NULL'), $count); + + $attempts = $overduehander->get_list_of_overdue_attempts(0); // before all attempts + $count = 0; + foreach ($attempts as $attempt) { + $count++; + } + $this->assertEquals(0, $count); + + } + + /** + * Test the group event handlers + */ + public function test_group_event_handlers() { + global $DB,$CFG; + + $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)); + $this->assertTrue(groups_add_member($group1, $user1)); + $this->assertTrue(groups_add_member($group2, $user1)); + + $uniqueid = 0; + + $quiz_generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + + $quiz = $quiz_generator->create_instance(array('course'=>$course->id, 'timeclose'=>1200, 'timelimit'=>0)); + + // add a group1 override + $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group1->id, 'timeclose'=>1300, 'timelimit'=>null)); + + // add an attempt + $attemptid = $DB->insert_record('quiz_attempts', array('quiz'=>$quiz->id, 'userid'=>$user1->id, 'state'=>'inprogress', 'timestart'=>100, 'timecheckstate'=>0, 'layout'=>'', 'uniqueid'=>$uniqueid++)); + + // update timecheckstate + quiz_update_open_attempts(array('quizid'=>$quiz->id)); + $this->assertEquals(1300, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + + // remove from group + $this->assertTrue(groups_remove_member($group1, $user1)); + $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + + // add back to group + $this->assertTrue(groups_add_member($group1, $user1)); + $this->assertEquals(1300, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + + // delete group + groups_delete_group($group1); + $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + $this->assertEquals(0, $DB->count_records('quiz_overrides', array('quiz'=>$quiz->id))); + + // add a group2 override + $DB->insert_record('quiz_overrides', array('quiz'=>$quiz->id, 'groupid'=>$group2->id, 'timeclose'=>1400, 'timelimit'=>null)); + quiz_update_open_attempts(array('quizid'=>$quiz->id)); + $this->assertEquals(1400, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + + // delete user1 from all groups + groups_delete_group_members($course->id, $user1->id); + $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + + // add back to group2 + $this->assertTrue(groups_add_member($group2, $user1)); + $this->assertEquals(1400, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + + // delete everyone from all groups + groups_delete_group_members($course->id); + $this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid))); + } +} \ No newline at end of file diff --git a/mod/quiz/tests/generator/lib.php b/mod/quiz/tests/generator/lib.php new file mode 100644 index 00000000000..614640bb181 --- /dev/null +++ b/mod/quiz/tests/generator/lib.php @@ -0,0 +1,105 @@ +. + +/** + * mod_quiz data generator + * + * @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(); + + +/** + * Quiz module PHPUnit data generator class + * + * @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_generator extends phpunit_module_generator { + + /** + * Create new quiz module instance + * @param array|stdClass $record + * @param array $options (mostly course_module properties) + * @return stdClass activity record with extra cmid field + */ + public function create_instance($record = null, array $options = null) { + global $CFG; + require_once("$CFG->dirroot/mod/quiz/locallib.php"); + + $this->instancecount++; + $i = $this->instancecount; + + $record = (object)(array)$record; + $options = (array)$options; + + if (empty($record->course)) { + throw new coding_exception('module generator requires $record->course'); + } + if (!isset($record->name)) { + $record->name = get_string('pluginname', 'quiz').' '.$i; + } + if (!isset($record->intro)) { + $record->intro = 'Test quiz '.$i; + } + if (!isset($record->introformat)) { + $record->introformat = FORMAT_MOODLE; + } + if (!isset($record->overduehandling)) { + $record->overduehandling = 'autoabandon'; + } + if (!isset($record->preferredbehavior)) { + $record->preferredbehavior = 'deferredfeedback'; + } + if (!isset($record->grade)) { + $record->grade = 100; + } + if (!isset($record->quizpassword)) { + $record->quizpassword = ''; + } + if (!isset($record->feedbackboundarycount)) { + $record->feedbackboundarycount = -1; + } + if (!isset($record->timeopen)) { + $record->timeopen = 0; + } + if (!isset($record->timeclose)) { + $record->timeclose = 0; + } + if (!isset($record->timelimit)) { + $record->timelimit = 0; + } + if (!isset($record->questiondecimalpoints)) { + $record->questiondecimalpoints = -2; + } + if (isset($options['idnumber'])) { + $record->cmidnumber = $options['idnumber']; + } else { + $record->cmidnumber = ''; + } + + $record->coursemodule = $this->precreate_course_module($record->course, $options); + $id = quiz_add_instance($record); + return $this->post_add_instance($id, $record->coursemodule); + } + +} diff --git a/mod/quiz/tests/generator_test.php b/mod/quiz/tests/generator_test.php new file mode 100644 index 00000000000..4ae705ad677 --- /dev/null +++ b/mod/quiz/tests/generator_test.php @@ -0,0 +1,63 @@ +. + +/** + * PHPUnit data generator 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(); + + +/** + * PHPUnit data generator testcase + * + * @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_generator_testcase extends advanced_testcase { + public function test_generator() { + global $DB, $SITE; + + $this->resetAfterTest(true); + + $this->assertEquals(0, $DB->count_records('quiz')); + + /** @var mod_quiz_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $this->assertInstanceOf('mod_quiz_generator', $generator); + $this->assertEquals('quiz', $generator->get_modulename()); + + $generator->create_instance(array('course'=>$SITE->id)); + $generator->create_instance(array('course'=>$SITE->id)); + $quiz = $generator->create_instance(array('course'=>$SITE->id)); + $this->assertEquals(3, $DB->count_records('quiz')); + + $cm = get_coursemodule_from_instance('quiz', $quiz->id); + $this->assertEquals($quiz->id, $cm->instance); + $this->assertEquals('quiz', $cm->modname); + $this->assertEquals($SITE->id, $cm->course); + + $context = context_module::instance($cm->id); + $this->assertEquals($quiz->cmid, $context->instanceid); + } +} diff --git a/mod/quiz/version.php b/mod/quiz/version.php index 59c7b3c332d..cbd42c2f164 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); -$module->version = 2012061703; // The current module version (Date: YYYYMMDDXX). +$module->version = 2012100801; // The current module version (Date: YYYYMMDDXX). $module->requires = 2012061700; // Requires this Moodle version. $module->component = 'mod_quiz'; // Full name of the plugin (used for diagnostics). $module->cron = 60;