MDL-35717 quiz: fix overdue attempt processing

This commit is contained in:
Matt Petro 2012-10-09 13:48:30 -04:00
parent 6109f2112c
commit 8e771aed93
31 changed files with 1066 additions and 161 deletions

View File

@ -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;
}

View File

@ -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;
}
/**

View File

@ -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));

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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() {

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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));

View File

@ -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 File

@ -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);

View File

@ -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.

View File

@ -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)) {

View File

@ -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));
}
}

View File

@ -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.

View File

@ -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 File

@ -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 File

@ -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');

View File

@ -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.

View File

@ -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 File

@ -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',

View File

@ -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;

View File

@ -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());
}
/**

View File

@ -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)) {

View File

@ -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) {
$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)));
}
}

View File

@ -0,0 +1,105 @@
<?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/>.
/**
* 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);
}
}

View File

@ -0,0 +1,63 @@
<?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/>.
/**
* 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);
}
}

View File

@ -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;