diff --git a/mod/quiz/accessrules.php b/mod/quiz/accessrules.php new file mode 100644 index 00000000000..5069d0c6945 --- /dev/null +++ b/mod/quiz/accessrules.php @@ -0,0 +1,616 @@ +_quiz = $quiz; + $this->_timenow = $timenow; + $this->create_standard_rules($canignoretimelimits); + } + + private function create_standard_rules($canignoretimelimits) { + if ($this->_quiz->attempts > 0) { + $this->_rules[] = new num_attempts_access_rule($this->_quiz, $this->_timenow); + } + $this->_rules[] = new open_close_date_access_rule($this->_quiz, $this->_timenow); + if ($this->_quiz->timelimit && !$canignoretimelimits) { + $this->_rules[] = new time_limit_access_rule($this->_quiz, $this->_timenow); + } + if ($this->_quiz->delay1 || $this->_quiz->delay2) { + $this->_rules[] = new inter_attempt_delay_access_rule($this->_quiz, $this->_timenow); + } + if ($this->_quiz->subnet) { + $this->_rules[] = new ipaddress_access_rule($this->_quiz, $this->_timenow); + } + if ($this->_quiz->password) { + $this->_passwordrule = new password_access_rule($this->_quiz, $this->_timenow); + $this->_rules[] = $this->_passwordrule; + } + if ($this->_quiz->popup) { + $this->_securewindowrule = new securewindow_access_rule($this->_quiz, $this->_timenow); + $this->_rules[] = $this->_securewindowrule; + } + } + + private function accumulate_messages(&$messages, $new) { + if (is_array($new)) { + $messages = array_merge($messages, $new); + } else if (is_string($new) && $new) { + $messages[] = $new; + } + } + + /** + * Print each message in an array, each surrounded by <p>, </p> tags. + * + * @param array $messages the array of message strings. + * @param boolean $return if true, return a string, instead of outputting. + * + * @return mixed, if $return is true, return the string that would have been output, otherwise + * return null. + */ + public function print_messages($messages, $return=false) { + $output = ''; + foreach ($messages as $message) { + $output .= '

' . $message . "

\n"; + } + if ($return) { + return $output; + } else { + echo $output; + } + } + + /** + * Provide a description of the rules that apply to this quiz, such + * as is shown at the top of the quiz view page. Note that not all + * rules consider themselves important enough to output a description. + * + * @return array an array of description messages which may be empty. It + * would be sensible to output each one surrounded by <p> tags. + */ + public function describe_rules() { + $result = array(); + foreach ($this->_rules as $rule) { + $this->accumulate_messages($result, $rule->description()); + } + return $result; + } + + /** + * Is it OK to let the current user start a new attempt now? If there are + * any restrictions in force now, return an array of reasons why access + * should be blocked. If access is OK, return false. + * + * @param integer $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return mixed An array of reason why access is not allowed, or an empty array + * (== false) if access should be allowed. + */ + public function prevent_new_attempt($numprevattempts, $lastattempt) { + $reasons = array(); + foreach ($this->_rules as $rule) { + $this->accumulate_messages($reasons, + $rule->prevent_new_attempt($numprevattempts, $lastattempt)); + } + return $reasons; + } + + /** + * Is it OK to let the current user start a new attempt now? If there are + * any restrictions in force now, return an array of reasons why access + * should be blocked. If access is OK, return false. + * + * @return mixed An array of reason why access is not allowed, or an empty array + * (== false) if access should be allowed. + */ + public function prevent_access() { + $reasons = array(); + foreach ($this->_rules as $rule) { + $this->accumulate_messages($reasons, $rule->prevent_access()); + } + return $reasons; + } + + /** + * Do any of the rules mean that this student will no be allowed any further attempts at this + * quiz. Used, for example, to change the label by the grade displayed on the view page from + * 'your current score is' to 'your final score is'. + * + * @param integer $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return boolean true if there is no way the user will ever be allowed to attempt this quiz again. + */ + public function is_finished($numprevattempts, $lastattempt) { + foreach ($this->_rules as $rule) { + if ($rule->is_finished($numprevattempts, $lastattempt)) { + return true; + } + } + return false; + } + + public function setup_secure_page() { + /// This prevents the message window coming up. + define('MESSAGE_WINDOW', true); + echo "\n\n", '\n"; + } + + public function show_attempt_timer_if_needed($attempt, $timenow) { + $timeleft = false; + foreach ($this->_rules as $rule) { + $ruletimeleft = $rule->time_left($attempt, $timenow); + if ($ruletimeleft !== false && ($timeleft === false || $ruletimeleft < $timeleft)) { + $timeleft = $ruletimeleft; + } + } + 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); + print_box_start('', 'quiz-timer-outer'); + print_heading(get_string('timeleft', 'quiz'), '', 3); + echo '

'; + print_box_end(); + echo "\n\n", '\n"; + } + } + + /** + * @return bolean if this quiz should only be shown to students in a secure window. + */ + public function securewindow_required($canpreview) { + return !$canpreview && !is_null($this->_securewindowrule); + } + + /** + * @return object the securewindow_access_rule instance for this quiz, + * or null if securewindow_required returns false. + */ + public function get_securewindow_object() { + return $this->_securewindowrule; + } + + /** + * @return bolean if this quiz is password protected. + */ + public function password_required() { + return !is_null($this->_passwordrule); + } + + /** + * Clear the flag in the session that says that the current user is allowed to do this quiz. + */ + public function clear_password_access() { + if (!is_null($this->_passwordrule)) { + $this->_passwordrule->clear_access_allowed(); + } + } + + /** + * Actually ask the user for the password, if they have not already given it this session. + * This function only returns is access is OK. + */ + public function do_password_check() { + if (!is_null($this->_passwordrule)) { + $this->_passwordrule->do_password_check(); + } + } + + /** + * @return string if the quiz policies merit it, return a warning string to be displayed + * in a javascript alert on the start attempt button. + */ + public function confirm_start_attempt_message() { + if ($this->_quiz->timelimit && $this->_quiz->attempts) { + return get_string('confirmstartattempttimelimit','quiz', $this->_quiz->attempts); + } else if ($this->_quiz->timelimit) { + return get_string('confirmstarttimelimit','quiz'); + } else if ($this->_quiz->attempts) { + return get_string('confirmstartattemptlimit','quiz', $this->_quiz->attempts); + } + return ''; + } + + /** + * Make some text into a link to review the quiz, if that is appropriate. + * + * @param string $linktext some text. + * @param object $attempt the attempt object + * @return string some HTML, the $linktext either unmodified or wrapped in + */ + public function make_review_link($linktext, $attempt) { + global $CFG; + + /// If not even responses are to be shown in review then we don't allow any review + if (!($this->_quiz->review & QUIZ_REVIEW_RESPONSES)) { + return $linktext; + } + + /// If the quiz is still open, are reviews allowed? + if ((!$this->_quiz->timeclose || time() < $this->_quiz->timeclose) && + !($this->_quiz->review & QUIZ_REVIEW_OPEN)) { + /// If not, don't link. + return $linktext; + } + + /// If the quiz is closed, are reviews allowed? + if (($this->_quiz->timeclose && time() > $this->_quiz->timeclose) && + !($this->_quiz->review & QUIZ_REVIEW_CLOSED)) { + /// If not, don't link. + return $linktext; + } + + /// If the attempt is still open, don't link. + if (!$attempt->timefinish) { + return $linktext; + } + + /// It is OK to link. + // TODO replace this with logic that matches review.php. + if ($this->securewindow_required(false)) { + return $this->get_securewindow_object()->make_review_link($linktext, $attempt->id); + } else { + return '' . $linktext . ''; + } + } +} + +/** + * A base class that defines the interface for the various quiz access rules. + * Most of the methods are defined in a slightly unnatural way because we either + * want to say that access is allowed, or explain the reason why it is block. + * Therefore instead of is_access_allowed(...) we have prevent_access(...) that + * return false if access is permitted, or a string explanation (which is treated + * as true) if access should be blocked. Slighly unnatural, but acutally the easist + * way to implement this. + */ +abstract class quiz_access_rule_base { + protected $_quiz; + protected $_timenow; + /** + * Create an instance of this rule for a particular quiz. + * @param object $quiz the quiz we will be controlling access to. + */ + public function __construct($quiz, $timenow) { + $this->_quiz = $quiz; + $this->_timenow = $timenow; + } + /** + * Whether or not a user should be allowed to start a new attempt at this quiz now. + * @param integer $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return string false if access should be allowed, a message explaining the reason if access should be prevented. + */ + public function prevent_new_attempt($numprevattempts, $lastattempt) { + return false; + } + /** + * Whether or not a user should be allowed to start a new attempt at this quiz now. + * @return string false if access should be allowed, a message explaining the reason if access should be prevented. + */ + public function prevent_access() { + return false; + } + /** + * Information, such as might be shown on the quiz view page, relating to this restriction. + * There is no obligation to return anything. If it is not appropriate to tell students + * about this rule, then just return ''. + * @return mixed a message, or array of messages, explaining the restriction + * (may be '' if no message is appropriate). + */ + public function description() { + return ''; + } + /** + * If this rule can determine that this user will never be allowed another attempt at + * this quiz, then return true. This is used so we can know whether to display a + * final score on the view page. This will only be called if there is not a currently + * active attempt for this user. + * @param integer $numattempts the number of previous attempts this user has made. + * @param object $lastattempt information about the user's last completed attempt. + * @return boolean true if this rule means that this user will never be allowed another + * attempt at this quiz. + */ + public function is_finished($numprevattempts, $lastattempt) { + return false; + } + + /** + * If, becuase 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. + * @param object $attempt the current attempt + * @param integer $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. + */ + public function time_left($attempt, $timenow) { + return false; + } +} + +/** + * A rule controlling the number of attempts allowed. + */ +class num_attempts_access_rule extends quiz_access_rule_base { + public function description() { + return get_string('attemptsallowedn', 'quiz', $this->_quiz->attempts); + } + public function prevent_new_attempt($numprevattempts, $lastattempt) { + if ($numprevattempts >= $this->_quiz->attempts) { + return get_string('nomoreattempts', 'quiz'); + } + return false; + } + public function is_finished($numprevattempts, $lastattempt) { + return $numprevattempts >= $this->_quiz->attempts; + } +} + +/** + * A rule enforcing open and close dates. + */ +class open_close_date_access_rule extends quiz_access_rule_base { + public function description() { + $result = array(); + if ($this->_timenow < $this->_quiz->timeopen) { + $result[] = get_string('quiznotavailable', 'quiz', userdate($this->_quiz->timeopen)); + } else if ($this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose) { + $result[] = get_string("quizclosed", "quiz", userdate($this->_quiz->timeclose)); + } else { + if ($this->_quiz->timeopen) { + $result[] = get_string('quizopenedon', 'quiz', userdate($this->_quiz->timeopen)); + } + if ($this->_quiz->timeclose) { + $result[] = get_string('quizcloseson', 'quiz', userdate($this->_quiz->timeclose)); + } + } + return $result; + } + public function prevent_access() { + if ($this->_timenow < $this->_quiz->timeopen || + ($this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose)) { + return get_string('notavailable', 'quiz'); + } + return false; + } + public function is_finished($numprevattempts, $lastattempt) { + return $this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose; + } + public function time_left($attempt, $timenow) { + if ($this->_quiz->timeclose) { + $timeleft = $this->_quiz->timeclose - $timenow; + if ($timeleft < QUIZ_SHOW_TIME_BEFORE_DEADLINE) { + return $timeleft; + } + } + return false; + } +} + +/** + * A rule imposing the delay between attemtps settings. + */ +class inter_attempt_delay_access_rule extends quiz_access_rule_base { + public function prevent_new_attempt($numprevattempts, $lastattempt) { + if ($this->_quiz->attempts > 0 && $numprevattempts >= $this->_quiz->attempts) { + /// No more attempts allowed anyway. + return false; + } + if ($this->_quiz->timeclose != 0 && $this->_timenow > $this->_quiz->timeclose) { + /// No more attempts allowed anyway. + return false; + } + $nextstarttime = 0; + if ($numprevattempts == 1 && $this->_quiz->delay1) { + $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay1; + } else if ($numprevattempts > 1 && $this->_quiz->delay2) { + $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay2; + } + if ($this->_timenow < $nextstarttime) { + if ($this->_quiz->timeclose == 0 || $nextstarttime <= $this->_quiz->timeclose) { + return get_string('youmustwait', 'quiz', userdate($nextstarttime)); + } else { + return get_string('youcannotwait', 'quiz'); + } + } + return false; + } + public function is_finished($numprevattempts, $lastattempt) { + $nextstarttime = 0; + if ($numprevattempts == 1 && $this->_quiz->delay1) { + $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay1; + } else if ($numprevattempts > 1 && $this->_quiz->delay2) { + $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay2; + } + return $this->_timenow <= $nextstarttime && + $this->_quiz->timeclose != 0 && $nextstarttime >= $this->_quiz->timeclose; + } +} + +/** + * A rule implementing the ipaddress check against the ->submet setting. + */ +class ipaddress_access_rule extends quiz_access_rule_base { + public function prevent_access() { + if (address_in_subnet(getremoteaddr(), $this->_quiz->subnet)) { + return false; + } else { + return get_string('subnetwrong', 'quiz'); + } + } +} + +/** + * A rule representing the password check. It does not actually implement the check, + * that has to be done directly in attempt.php, but this facilitates telling users about it. + */ +class password_access_rule extends quiz_access_rule_base { + public function description() { + return get_string('requirepasswordmessage', 'quiz'); + } + /** + * Clear the flag in the session that says that the current user is allowed to do this quiz. + */ + public function clear_access_allowed() { + global $SESSION; + if (!empty($SESSION->passwordcheckedquizzes[$this->_quiz->id])) { + unset($SESSION->passwordcheckedquizzes[$this->_quiz->id]); + } + } + /** + * Actually ask the user for the password, if they have not already given it this session. + * This function only returns is access is OK. + * + * @param $return if true, return the HTML for the form (if required), instead of outputting + * it at stopping + * @return mixed return null, unless $return is true, and a form needs to be displayed. + */ + public function do_password_check($return = false) { + global $CFG, $SESSION; + + /// We have already checked the password for this quiz this session, so don't ask again. + if (!empty($SESSION->passwordcheckedquizzes[$this->_quiz->id])) { + return; + } + + /// If the user cancelled the password form, send them back to the view page. + if (optional_param('cancelpassword', false, PARAM_BOOL)) { + redirect($CFG->wwwroot . '/mod/quiz/view.php?q=' . $this->_quiz->id); + } + + /// If they entered the right password, let them in. + $enteredpassword = optional_param('quizpassword', '', PARAM_RAW); + if (strcmp($this->_quiz->password, $enteredpassword) === 0) { + $SESSION->passwordcheckedquizzes[$this->_quiz->id] = true; + return; + } + + /// User entered the wrong password, or has not entered one yet, so display the form. + $output = ''; + + /// Start the page and print the quiz intro, if any. + if (!$return) { + print_header('', '', '', 'quizpassword'); + } + if (trim(strip_tags($this->_quiz->intro))) { + $formatoptions->noclean = true; + $output .= print_box(format_text($this->_quiz->intro, FORMAT_MOODLE, $formatoptions), + 'generalbox', 'intro', true); + } + $output .= print_box_start('generalbox', 'passwordbox', true); + + /// If they have previously tried and failed to enter a password, tell them it was wrong. + if (!empty($enteredpassword)) { + $output .= '

' . get_string('passworderror', 'quiz') . '

'; + } + + /// Print the password entry form. + $output .= '

' . get_string('requirepasswordmessage', 'quiz') . "

\n"; + $output .= '
' . "\n"; + $output .= "
\n"; + $output .= '\n"; + $output .= '' . "\n"; + $output .= ''; + $output .= '' . "\n"; + $output .= "
\n"; + $output .= "
\n"; + + /// Finish page. + $output .= print_box_end(true); + + /// return or display form. + if ($return) { + return $output; + } else { + echo $output; + print_footer('empty'); + exit; + } + } +} + +/** + * A rule representing the time limit. It does not actually restrict access, but we use this + * class to encapsulate some of the relevant code. + */ +class time_limit_access_rule extends quiz_access_rule_base { + public function description() { + return get_string('quiztimelimit', 'quiz', format_time($this->_quiz->timelimit * 60)); + } + public function time_left($attempt, $timenow) { + return $attempt->timestart + $this->_quiz->timelimit*60 - $timenow; + } +} + +/** + * A rule implementing the ipaddress check against the ->submet setting. + */ +class securewindow_access_rule extends quiz_access_rule_base { + private $windowoptions = "left=0, top=0, height='+window.screen.height+', width='+window.screen.width+', channelmode=yes, fullscreen=yes, scrollbars=yes, resizeable=no, directories=no, toolbar=no, titlebar=no, location=no, status=no, menubar=no"; + + /** + * Output the start attempt button. + * + * @param string $buttontext the desired button caption. + * @param string $cmid the quiz cmid. + * @param string $strconfirmstartattempt optional message to diplay in a JavaScript altert + * before the button submits. + */ + public function print_start_attempt_button($buttontext, $cmid, $strconfirmstartattempt) { + global $CFG; + $attempturl = $CFG->wwwroot . '/mod/quiz/attempt.php?id=' . $cmid; + $window = 'quizpopup'; + + if (!empty($CFG->usesid) && !isset($_COOKIE[session_name()])) { + $attempturl = sid_process_url($attempturl); + } + + echo 'windowoptions');", '" />'; + } + + /** + * Make a link to the review page for an attempt. + * + * @param string $linktext the desired link text. + * @param integer $attemptid the attempt id. + * @return string HTML for the link. + */ + public function make_review_link($linktext, $attemptid) { + global $CFG; + return link_to_popup_window($CFG->wwwroot . '/mod/quiz/review.php?q=' . $this->_quiz->id . + '&attempt=' . $attemptid, 'quizpopup', $linktext, '', '', '', $windowoptions, true); + } +} +?> \ No newline at end of file diff --git a/mod/quiz/simpletest/testaccessrules.php b/mod/quiz/simpletest/testaccessrules.php new file mode 100644 index 00000000000..018a6f21863 --- /dev/null +++ b/mod/quiz/simpletest/testaccessrules.php @@ -0,0 +1,402 @@ +dirroot . '/mod/quiz/locallib.php'); + +class simple_rules_test extends UnitTestCase { + function test_num_attempts_access_rule() { + $quiz = new stdClass; + $quiz->attempts = 3; + $rule = new num_attempts_access_rule($quiz, 0); + $attempt = new stdClass; + + $this->assertEqual($rule->description(), get_string('attemptsallowedn', 'quiz', 3)); + + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('nomoreattempts', 'quiz')); + $this->assertEqual($rule->prevent_new_attempt(666, $attempt), get_string('nomoreattempts', 'quiz')); + + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->is_finished(2, $attempt)); + $this->assertTrue($rule->is_finished(3, $attempt)); + $this->assertTrue($rule->is_finished(666, $attempt)); + + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->time_left($attempt, 1)); + } + + function test_ipaddress_access_rule() { + $quiz = new stdClass; + $attempt = new stdClass; + + $quiz->subnet = getremoteaddr(); + $rule = new ipaddress_access_rule($quiz, 0); + $this->assertFalse($rule->prevent_access()); + $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)); + + $quiz->subnet = '0.0.0.0'; + $rule = new ipaddress_access_rule($quiz, 0); + $this->assertTrue($rule->prevent_access()); + $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)); + } + + function test_time_limit_access_rule() { + $quiz = new stdClass; + $quiz->timelimit = 60; + $rule = new time_limit_access_rule($quiz, 10000); + $attempt = new stdClass; + + $this->assertEqual($rule->description(), get_string('quiztimelimit', 'quiz', format_time(3600))); + + $attempt->timestart = 10000; + $this->assertEqual($rule->time_left($attempt, 10000), 3600); + $this->assertEqual($rule->time_left($attempt, 12000), 1600); + $this->assertEqual($rule->time_left($attempt, 14000), -400); + + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + } +} + +class open_close_date_access_rule_test extends UnitTestCase { + function test_no_dates() { + $quiz = new stdClass; + $quiz->timeopen = 0; + $quiz->timeclose = 0; + $attempt = new stdClass; + + $rule = new open_close_date_access_rule($quiz, 10000); + $this->assertFalse($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, 10000)); + $this->assertFalse($rule->time_left($attempt, 0)); + + $rule = new open_close_date_access_rule($quiz, 0); + $this->assertFalse($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)); + } + + function test_start_date() { + $quiz = new stdClass; + $quiz->timeopen = 10000; + $quiz->timeclose = 0; + $attempt = new stdClass; + + $rule = new open_close_date_access_rule($quiz, 9999); + $this->assertEqual($rule->description(), array(get_string('quiznotavailable', 'quiz', userdate(10000)))); + $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz')); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->time_left($attempt, 0)); + + $rule = new open_close_date_access_rule($quiz, 10000); + $this->assertEqual($rule->description(), array(get_string('quizopenedon', 'quiz', userdate(10000)))); + $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)); + } + + function test_close_date() { + $quiz = new stdClass; + $quiz->timeopen = 0; + $quiz->timeclose = 20000; + $attempt = new stdClass; + + $rule = new open_close_date_access_rule($quiz, 20000); + $this->assertEqual($rule->description(), array(get_string('quizcloseson', 'quiz', userdate(20000)))); + $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->assertEqual($rule->time_left($attempt, 19900), 100); + $this->assertEqual($rule->time_left($attempt, 20000), 0); + $this->assertEqual($rule->time_left($attempt, 20100), -100); + + $rule = new open_close_date_access_rule($quiz, 20001); + $this->assertEqual($rule->description(), array(get_string('quizclosed', 'quiz', userdate(20000)))); + $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz')); + $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->assertEqual($rule->time_left($attempt, 19900), 100); + $this->assertEqual($rule->time_left($attempt, 20000), 0); + $this->assertEqual($rule->time_left($attempt, 20100), -100); + } + + function test_both_dates() { + $quiz = new stdClass; + $quiz->timeopen = 10000; + $quiz->timeclose = 20000; + $attempt = new stdClass; + + $rule = new open_close_date_access_rule($quiz, 9999); + $this->assertEqual($rule->description(), array(get_string('quiznotavailable', 'quiz', userdate(10000)))); + $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz')); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + + $rule = new open_close_date_access_rule($quiz, 10000); + $this->assertEqual($rule->description(), array(get_string('quizopenedon', 'quiz', userdate(10000)), + get_string('quizcloseson', 'quiz', userdate(20000)))); + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + + $rule = new open_close_date_access_rule($quiz, 20000); + $this->assertEqual($rule->description(), array(get_string('quizopenedon', 'quiz', userdate(10000)), + get_string('quizcloseson', 'quiz', userdate(20000)))); + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + + $rule = new open_close_date_access_rule($quiz, 20001); + $this->assertEqual($rule->description(), array(get_string('quizclosed', 'quiz', userdate(20000)))); + $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz')); + $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->assertEqual($rule->time_left($attempt, 19900), 100); + $this->assertEqual($rule->time_left($attempt, 20000), 0); + $this->assertEqual($rule->time_left($attempt, 20100), -100); + } +} + +class inter_attempt_delay_access_rule_test extends UnitTestCase { + function test_just_first_delay() { + $quiz = new stdClass; + $quiz->attempts = 3; + $quiz->delay1 = 1000; + $quiz->delay2 = 0; + $quiz->timeclose = 0; + $attempt = new stdClass; + $attempt->timefinish = 10000; + + $rule = new inter_attempt_delay_access_rule($quiz, 10000); + $this->assertFalse($rule->description()); + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->time_left($attempt, 0)); + + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(3, $attempt)); + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11000))); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $attempt->timefinish = 9000; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $attempt->timefinish = 9001; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(10001))); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + } + + function test_just_second_delay() { + $quiz = new stdClass; + $quiz->attempts = 5; + $quiz->delay1 = 0; + $quiz->delay2 = 1000; + $quiz->timeclose = 0; + $attempt = new stdClass; + $attempt->timefinish = 10000; + + $rule = new inter_attempt_delay_access_rule($quiz, 10000); + $this->assertFalse($rule->description()); + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->time_left($attempt, 0)); + + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(5, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(11000))); + $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('youmustwait', 'quiz', userdate(11000))); + $attempt->timefinish = 9000; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(3, $attempt)); + $attempt->timefinish = 9001; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(10001))); + $this->assertEqual($rule->prevent_new_attempt(4, $attempt), get_string('youmustwait', 'quiz', userdate(10001))); + } + + function test_just_both_delays() { + $quiz = new stdClass; + $quiz->attempts = 5; + $quiz->delay1 = 2000; + $quiz->delay2 = 1000; + $quiz->timeclose = 0; + $attempt = new stdClass; + $attempt->timefinish = 10000; + + $rule = new inter_attempt_delay_access_rule($quiz, 10000); + $this->assertFalse($rule->description()); + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->time_left($attempt, 0)); + + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(5, $attempt)); + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(12000))); + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(11000))); + $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('youmustwait', 'quiz', userdate(11000))); + $attempt->timefinish = 8000; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(3, $attempt)); + $attempt->timefinish = 8001; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(10001))); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(4, $attempt)); + $attempt->timefinish = 9000; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11000))); + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $this->assertFalse($rule->prevent_new_attempt(3, $attempt)); + $attempt->timefinish = 9001; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11001))); + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(10001))); + $this->assertEqual($rule->prevent_new_attempt(4, $attempt), get_string('youmustwait', 'quiz', userdate(10001))); + } + + function test_with_close_date() { + $quiz = new stdClass; + $quiz->attempts = 5; + $quiz->delay1 = 2000; + $quiz->delay2 = 1000; + $quiz->timeclose = 15000; + $attempt = new stdClass; + $attempt->timefinish = 13000; + + $rule = new inter_attempt_delay_access_rule($quiz, 10000); + $this->assertFalse($rule->description()); + $this->assertFalse($rule->prevent_access()); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->time_left($attempt, 0)); + + $attempt->timefinish = 13000; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(15000))); + $attempt->timefinish = 13001; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youcannotwait', 'quiz')); + $attempt->timefinish = 14000; + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(15000))); + $attempt->timefinish = 14001; + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youcannotwait', 'quiz')); + + $rule = new inter_attempt_delay_access_rule($quiz, 15000); + $attempt->timefinish = 13000; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $attempt->timefinish = 13001; + $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youcannotwait', 'quiz')); + $attempt->timefinish = 14000; + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $attempt->timefinish = 14001; + $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youcannotwait', 'quiz')); + + $rule = new inter_attempt_delay_access_rule($quiz, 15001); + $attempt->timefinish = 13000; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $attempt->timefinish = 13001; + $this->assertFalse($rule->prevent_new_attempt(1, $attempt)); + $attempt->timefinish = 14000; + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + $attempt->timefinish = 14001; + $this->assertFalse($rule->prevent_new_attempt(2, $attempt)); + } +} + +class password_access_rule_test extends UnitTestCase { + function test_password_access_rule() { + $quiz = new stdClass; + $quiz->password = 'frog'; + $rule = new password_access_rule($quiz, 0); + $attempt = new stdClass; + + $this->assertFalse($rule->prevent_access()); + $this->assertEqual($rule->description(), get_string('requirepasswordmessage', 'quiz')); + $this->assertFalse($rule->prevent_new_attempt(0, $attempt)); + $this->assertFalse($rule->is_finished(0, $attempt)); + $this->assertFalse($rule->time_left($attempt, 1)); + } + + function test_do_password_check() { + $reqpwregex = '/' . preg_quote(get_string('requirepasswordmessage', 'quiz')) . '/'; + $pwerrregex = '/' . preg_quote(get_string('passworderror', 'quiz')) . '/'; + + $quiz = new stdClass; + $quiz->id = -1; // So as not to interfere with any real quizzes. + $quiz->intro = 'SOME INTRO TEXT'; + $quiz->password = 'frog'; + $rule = new password_access_rule($quiz, 0); + + $rule->clear_access_allowed(-1); + $_POST['cancelpassword'] = false; + $_POST['quizpassword'] = ''; + $html = $rule->do_password_check(true); + $this->assertPattern($reqpwregex, $html); + $this->assertPattern('/SOME INTRO TEXT/', $html); + $this->assertNoPattern($pwerrregex, $html); + + $_POST['quizpassword'] = 'toad'; + $html = $rule->do_password_check(true); + $this->assertPattern($reqpwregex, $html); + $this->assertPattern($pwerrregex, $html); + + $_POST['quizpassword'] = 'frog'; + $this->assertNull($rule->do_password_check(true)); + + // Check that once you are in, the password isn't checked again. + $_POST['quizpassword'] = 'newt'; + $this->assertNull($rule->do_password_check(true)); + + $rule->clear_access_allowed(-1); + $html = $rule->do_password_check(true); + $this->assertPattern($reqpwregex, $html); + } +} + +class securewindow_access_rule_test extends UnitTestCase { + // Nothing very testable in this class, just test that it obeys the general access rule contact. + + function test_securewindow_access_rule() { + $quiz = new stdClass; + $quiz->popup = 1; + $rule = new securewindow_access_rule($quiz, 0); + $attempt = new stdClass; + + $this->assertFalse($rule->prevent_access()); + $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)); + } +} + +class quiz_access_manager_test extends UnitTestCase { + // TODO +} +?>