From: tjhunt Date: Fri, 7 Mar 2008 12:33:07 +0000 (+0000) Subject: MDL-13806 - Refactor all the code that implements the rules for whether students... X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=c52c62d1b1294f4c9b6fcee1ae0845fca63e5af1;p=moodle.git MDL-13806 - Refactor all the code that implements the rules for whether students can attempt the quiz now into some classes. Resource page now done as well. That should be everything. --- diff --git a/lang/en_utf8/quiz.php b/lang/en_utf8/quiz.php index 3f75f4f15c..45ed7b689b 100644 --- a/lang/en_utf8/quiz.php +++ b/lang/en_utf8/quiz.php @@ -6,6 +6,7 @@ $string['2hours'] = '2 Hours'; $string['30minutes'] = '30 Minutes'; $string['6hours'] = '6 Hours'; $string['acceptederror'] = 'Accepted error'; +$string['accessnoticesheader'] = 'You can preview this quiz, but if this were a real attempt, you would be blocked because:'; $string['action'] = 'Action'; $string['adaptive'] = 'Adaptive mode'; $string['addcategory'] = 'Add category'; @@ -57,6 +58,7 @@ $string['attemptlast'] = 'Last attempt'; $string['attemptquiznow'] = 'Attempt quiz now'; $string['attempts'] = 'Attempts'; $string['attemptsallowed'] = 'Attempts allowed'; +$string['attemptsallowedn'] = 'Attempts allowed: $a'; $string['attemptsdeleted'] = 'Quiz attempts deleted'; $string['attemptselection'] = 'Select which attempts to analyze per user:'; $string['attemptsexist'] = 'You can no longer add or remove questions.'; @@ -77,6 +79,7 @@ $string['calculated'] = 'Calculated'; $string['calculatedquestion'] = 'Calculated Question not supported at line $a. The question will be ignored'; $string['cannotcreatepath'] = 'Path cannot be created ($a)'; $string['cannoteditafterattempts'] = 'You cannot add or remove questions because there are attempts.'; +$string['cannotfindprevattempt'] = 'Cannot find previous attempt to build on.'; $string['cannotinsert'] = 'Cannot insert question'; $string['cannotopen'] = 'Cannot open export file ($a)'; $string['cannotread'] = 'Cannot read import file (or file is empty)'; @@ -267,6 +270,7 @@ $string['gradeboundary'] = 'Grade boundary'; $string['gradeessays'] = 'Grade Essays'; $string['gradehighest'] = 'Highest grade'; $string['grademethod'] = 'Grading method'; +$string['gradingmethod'] = 'Grading method: $a'; $string['gradesdeleted'] = 'Quiz grades deleted'; $string['gradesofar'] = '$a->method: $a->mygrade / $a->quizgrade.'; $string['gradingdetails'] = 'Marks for this submission: $a->raw/$a->max.'; @@ -310,6 +314,7 @@ $string['learnwise'] = 'Learnwise format'; $string['link'] = 'Link'; $string['listitems'] = 'Listing of Items in Quiz'; $string['literal'] = 'Literal'; +$string['loadingquestionsfailed'] = 'Loading questions failed: $a'; $string['loguniform'] = 'digits, from a loguniform distribution'; $string['makecopy'] = 'Save as new question'; $string['managetypes'] = 'Manage question types and servers'; @@ -365,7 +370,7 @@ $string['noresponse'] = 'No Response'; $string['noreview'] = 'You are not allowed to review this quiz'; $string['noreviewuntil'] = 'You are not allowed to review this quiz until $a'; $string['noscript'] = 'JavaScript must be enabled to continue!'; -$string['notavailable'] = 'Sorry, this quiz is not available'; +$string['notavailable'] = 'This quiz is not currently available'; $string['notavailabletostudents'] = 'Note: This quiz is not currently available to your students'; $string['notenoughanswers'] = 'This type of question requires at least $a answers'; $string['notenoughsubquestions'] = 'Not enough sub-questions have been defined!
Do you want to go back and fix this question?'; @@ -434,9 +439,10 @@ $string['quizclose'] = 'Close the quiz'; $string['quizclosed'] = 'This quiz closed on $a'; $string['quizcloses'] = 'Quiz closes'; $string['quizcloseson'] = 'This quiz will close at $a'; -$string['quiznotavailable'] = 'The quiz will not be available until: $a'; +$string['quiznotavailable'] = 'The quiz will not be available until $a'; $string['quizopen'] = 'Open the quiz'; $string['quizopens'] = 'Quiz opens'; +$string['quizopenedon'] = 'This quiz opened at $a'; $string['quizsettings'] = 'Quiz settings'; $string['quiztimelimit'] = 'Time limit: $a'; $string['quiztimer'] = 'Quiz Timer'; @@ -491,6 +497,7 @@ $string['reviewbefore'] = 'Allow review while quiz is open'; $string['reviewclosed'] = 'After the quiz is closed'; $string['reviewimmediately'] = 'Immediately after the attempt'; $string['reviewnever'] = 'Never allow review'; +$string['reviewnotallowed'] = 'You are not allowed to review other users\' attempts at this quiz.'; $string['reviewofattempt'] = 'Review of attempt $a'; $string['reviewofpreview'] = 'Review of preview'; $string['reviewopen'] = 'Later, while the quiz is still open'; @@ -501,6 +508,7 @@ $string['rqp'] = 'Remote Question'; $string['rqps'] = 'Remote Questions'; $string['save'] = 'Save'; $string['saveandedit'] = 'Save changes and edit questions'; +$string['saveattemptfailed'] = 'Failed to save the current quiz attempt.'; $string['savedfromdeletedcourse'] = 'Saved from deleted course \"$a\"'; $string['savegrades'] = 'Save grades'; $string['savemyanswers'] = 'Save my answers'; @@ -547,6 +555,7 @@ $string['startedon'] = 'Started on'; $string['stoponerror'] = 'Stop on error'; $string['subneterror'] = 'Sorry, this quiz has been locked so that it is only accessible from certain locations. Currently your computer is not one of those allowed to use this quiz.'; $string['subnetnotice'] = 'This quiz has been locked so that it is only accessible from certain locations. Your computer is not on an allowed subnet. As teacher you are allowed to preview anyway.'; +$string['subnetwrong'] = 'This quiz is only accessible from certain locations, and this computer is not on the allowed list.'; $string['substitutedby'] = 'will be substituted by'; $string['summaryofattempts'] = 'Summary of your previous attempts'; $string['temporaryblocked'] = 'You are temporarily not allowed to re-attempt the quiz.
You will be able to take another attempt on:'; @@ -587,6 +596,7 @@ $string['warningmissingtype'] = 'This question is of a type that has not been $string['warningsdetected'] = '$a warning(s) detected'; $string['webct'] = 'WebCT format'; $string['wheregrade'] = 'Where\'s my grade?'; +$string['windowclosing'] = 'This window will close shortly.'; $string['wildcard'] = 'Wild card'; $string['withselected'] = 'With selected'; $string['withsummary'] = 'with Summary Statistics'; @@ -597,6 +607,8 @@ $string['xml'] = 'Moodle XML format'; $string['xmlimportnoname'] = 'Missing question name in xml file'; $string['xmlimportnoquestion'] = 'Missing question text in xml file'; $string['xmltypeunsupported'] = 'Question type $a is not supported by xml import'; +$string['youmustwait'] = 'You must wait before you may re-attempt this quiz. You will be allowed to start another attempt after $a.'; +$string['youcannotwait'] = 'This quiz closes before you will be allowed to start another attempt.'; $string['youneedtoenrol'] = 'You need to enrol in this course before you can attempt this quiz'; $string['yourfinalgradeis'] = 'Your final grade for this quiz is $a.'; $string['zerosignificantfiguresnotallowed'] = 'The correct answer cannot have zero significant figures!'; diff --git a/mod/quiz/accessrules.php b/mod/quiz/accessrules.php index 5069d0c694..2a2ea4e21f 100644 --- a/mod/quiz/accessrules.php +++ b/mod/quiz/accessrules.php @@ -14,7 +14,7 @@ class quiz_access_manager { * Create an instance for a particular quiz. * @param object $quiz the quiz we will be controlling access to. * @param integer $timenow the - * @param boolean $canpreview whether the current user has the + * @param boolean $canpreview whether the current user has the * @param boolean $ignoretimelimits */ public function __construct($quiz, $timenow, $canignoretimelimits) { @@ -60,7 +60,7 @@ class quiz_access_manager { * * @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. */ @@ -79,7 +79,7 @@ class quiz_access_manager { /** * 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. + * 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. @@ -145,16 +145,14 @@ class quiz_access_manager { return false; } - public function setup_secure_page() { - /// This prevents the message window coming up. - define('MESSAGE_WINDOW', true); - echo "\n\n", '\n"; + /** + * Do the printheader call, etc. required for a secure page, including the necessary JS. + * + * @param string $title HTML title tag content, passed to printheader. + * @param string $headtags extra stuff to go in the HTML head tag, passed to printheader. + */ + public function setup_secure_page($title, $headtags) { + $this->_securewindowrule->setup_secure_page($title, $headtags); } public function show_attempt_timer_if_needed($attempt, $timenow) { @@ -166,7 +164,7 @@ class quiz_access_manager { } } if ($timeleft !== false) { - /// Make sure the timer starts just above zero. If $timeleft was <= 0, then + /// 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'); @@ -187,11 +185,99 @@ class quiz_access_manager { } /** - * @return object the securewindow_access_rule instance for this quiz, - * or null if securewindow_required returns false. + * Print a button to start a quiz attempt, with an appropriate javascript warning, + * depending on the access restrictions. The link will pop up a 'secure' window, if + * necessary. The button will initially be hidden, with JavaScript to reveal it, and + * a noscript tag saying that the quiz requires JavaScript. + * + * @param boolean $canpreview whether this user can preview. This affects whether they must + * use a secure window. + * @param string $buttontext the label to put on the button. + * @param boolean $unfinished whether the button is to continue an existing attempt, + * or start a new one. This affects whether a javascript alert is shown. */ - public function get_securewindow_object() { - return $this->_securewindowrule; + public function print_start_attempt_button($canpreview, $buttontext, $unfinished) { + /// Do we need a confirm javascript alert? + if ($unfinished) { + $strconfirmstartattempt = ''; + } else { + $strconfirmstartattempt = $this->confirm_start_attempt_message(); + } + + /// Show the start button, in a div that is initially hidden. + require_js('yui_yahoo'); + require_js('yui_event'); + echo '\n"; + + /// JavaScript to reveal the button. + echo '\n"; + + /// A noscript tag to explains that the quiz only works with JavaScript enabled. + echo '\n"; + } + + /** + * Send the user back to the quiz view page. Normally this is just a redirect, but + * If we were in a secure window, we close this window, and reload the view window we came from. + * + * @param boolean $canpreview This affects whether we have to worry about secure window stuff. + */ + public function back_to_view_page($canpreview, $message = '') { + global $CFG; + $url = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $this->_quiz->id; + if (securewindow_required($canpreview)) { + print_header(); + print_box_start(); + if ($message) { + echo '

' . $message . '

' . get_string('windowclosing', 'quiz') . '

'; + $delay = 5; + } else { + echo '

' . get_string('pleaseclose', 'quiz') . '

'; + $delay = 0; + } + print_box_end(); + echo ''; + print_footer('empty'); + die(); + } else { + redirect($url, $message); + } + } + + /** + * Print a control to finish the review. Normally this is just a link, but if we are + * in a secure window, it needs to be a button that does quiz_secure_window.close. + * + * @param boolean $canpreview This affects whether we have to worry about secure window stuff. + */ + public function print_finish_review_link($canpreview) { + global $CFG; + $url = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $this->_quiz->id; + echo '
'; + if ($this->securewindow_required($canpreview)) { + $url = addslashes_js(htmlspecialchars($url)); + echo '\n"; + } else { + echo '' . get_string('finishreview', 'quiz') . "\n"; + } + echo "
\n"; } /** @@ -213,10 +299,12 @@ class quiz_access_manager { /** * 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 boolean $canpreview used to enfore securewindow stuff. */ - public function do_password_check() { + public function do_password_check($canpreview) { if (!is_null($this->_passwordrule)) { - $this->_passwordrule->do_password_check(); + $this->_passwordrule->do_password_check($canpreview, $this); } } @@ -240,44 +328,41 @@ class quiz_access_manager { * * @param string $linktext some text. * @param object $attempt the attempt object - * @return string some HTML, the $linktext either unmodified or wrapped in + * @return string some HTML, the $linktext either unmodified or wrapped in a link to the review page. */ - public function make_review_link($linktext, $attempt) { + public function make_review_link($linktext, $attempt, $canpreview, $reviewoptions) { 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. + /// If review of responses is not allowed, or the attempt is still open, don't link. + if (!$reviewoptions->responses || !$attempt->timefinish) { 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); + /// It is OK to link, does it need to be in a secure window? + if ($this->securewindow_required($canpreview)) { + return $this->_securewindowrule->make_review_link($linktext, $attempt->id); } else { return '' . $linktext . ''; } } + /** + * If $reviewoptions->responses is false, meaning that students can't review this + * attempt at the moment, return an appropriate string explaining why. + * + * @param object $reviewoptions as obtained from quiz_get_reviewoptions. + * @return string an appropraite message. + */ + public function cannot_review_message($reviewoptions) { + if ($reviewoptions->quizstate == QUIZ_STATE_IMMEDIATELY) { + return ''; + } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $this->_quiz->timeclose && + ($this->_quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) { + return get_string('noreviewuntil', 'quiz', userdate($this->_quiz->timeclose)); + } else { + return get_string('noreview', 'quiz'); + } + } } /** @@ -328,7 +413,7 @@ abstract class quiz_access_rule_base { } /** * 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 + * 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. @@ -486,21 +571,23 @@ class password_access_rule extends quiz_access_rule_base { * 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 + * @param boolean $canpreview used to enfore securewindow stuff. + * @param object $accessmanager the accessmanager calling us. + * @param boolean $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) { + public function do_password_check($canpreview, $accessmanager, $return = false) { global $CFG, $SESSION; - /// We have already checked the password for this quiz this session, so don't ask again. + /// 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 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); + $accessmanager->back_to_view_page($canpreview); } /// If they entered the right password, let them in. @@ -580,15 +667,14 @@ class securewindow_access_rule extends quiz_access_rule_base { * 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) { + public function print_start_attempt_button($buttontext, $strconfirmstartattempt) { global $CFG; - $attempturl = $CFG->wwwroot . '/mod/quiz/attempt.php?id=' . $cmid; + $attempturl = $CFG->wwwroot . '/mod/quiz/attempt.php?q=' . $this->_quiz->id; $window = 'quizpopup'; - + if (!empty($CFG->usesid) && !isset($_COOKIE[session_name()])) { $attempturl = sid_process_url($attempturl); } @@ -612,5 +698,24 @@ class securewindow_access_rule extends quiz_access_rule_base { return link_to_popup_window($CFG->wwwroot . '/mod/quiz/review.php?q=' . $this->_quiz->id . '&attempt=' . $attemptid, 'quizpopup', $linktext, '', '', '', $windowoptions, true); } + + /** + * Do the printheader call, etc. required for a secure page, including the necessary JS. + * + * @param string $title HTML title tag content, passed to printheader. + * @param string $headtags extra stuff to go in the HTML head tag, passed to printheader. + */ + public function setup_secure_page($title, $headtags) { + /// This prevents the message window coming up. + define('MESSAGE_WINDOW', true); + print_header($title, '', '', '', $headtags, false, '', '', false, ''); + echo "\n\n", '\n"; + } } ?> \ No newline at end of file diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 56c16f01ec..bbd185e1ac 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -10,14 +10,14 @@ * @package quiz */ - require_once("../../config.php"); - require_once("locallib.php"); + require_once('../../config.php'); + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - // remember the current time as the time any responses were submitted - // (so as to make sure students don't get penalized for slow processing on this page) - $timestamp = time(); +/// remember the current time as the time any responses were submitted +/// (so as to make sure students don't get penalized for slow processing on this page) + $timenow = time(); - // Get submitted parameters. +/// Get submitted parameters. $id = optional_param('id', 0, PARAM_INT); // Course Module ID $q = optional_param('q', 0, PARAM_INT); // or quiz ID $page = optional_param('page', 0, PARAM_INT); @@ -48,228 +48,149 @@ } } - // We treat automatically closed attempts just like normally closed attempts +/// We treat automatically closed attempts just like normally closed attempts if ($timeup) { $finishattempt = 1; } +/// Check login and get contexts. require_login($course->id, false, $cm); - - $coursecontext = get_context_instance(CONTEXT_COURSE, $cm->course); // course context + $coursecontext = get_context_instance(CONTEXT_COURSE, $cm->course); $context = get_context_instance(CONTEXT_MODULE, $cm->id); - $ispreviewing = has_capability('mod/quiz:preview', $context); + $canpreview = has_capability('mod/quiz:preview', $context); - // if no questions have been set up yet redirect to edit.php - if (!$quiz->questions and has_capability('mod/quiz:manage', $context)) { - redirect($CFG->wwwroot . '/mod/quiz/edit.php?quizid=' . $quiz->id); +/// Create an object to manage all the other (non-roles) access rules. + $accessmanager = new quiz_access_manager($quiz, $timenow, + has_capability('mod/quiz:ignoretimelimits', $context, NULL, false)); + if ($canpreview && $forcenew) { + $accessmanager->clear_password_access(); } - if (!$ispreviewing) { - require_capability('mod/quiz:attempt', $context); +/// if no questions have been set up yet redirect to edit.php + if (!$quiz->questions && has_capability('mod/quiz:manage', $context)) { + redirect($CFG->wwwroot . '/mod/quiz/edit.php?quizid=' . $quiz->id); } -/// Get number for the next or unfinished attempt - if(!$attemptnumber = (int)get_field_sql('SELECT MAX(attempt)+1 FROM ' . - "{$CFG->prefix}quiz_attempts WHERE quiz = '{$quiz->id}' AND " . - "userid = '{$USER->id}' AND timefinish > 0 AND preview != 1")) { - $attemptnumber = 1; +/// Check capabilites. + if (!$canpreview) { + require_capability('mod/quiz:attempt', $context); } +/// We intentionally do not check otehr access rules until after we have processed +/// any submitted responses (which would be sesskey protected). This is so that when +/// someone submits close to the exact moment when the quiz closes, there responses are not lost. - $strattemptnum = get_string('attempt', 'quiz', $attemptnumber); - $strquizzes = get_string("modulenameplural", "quiz"); - $popup = $quiz->popup && !$ispreviewing; // Controls whether this is shown in a javascript-protected window. - -/// We intentionally do not check open and close times here. Instead we do it lower down. -/// This is to deal with what happens when someone submits close to the exact moment when the quiz closes. +/// Load attempt or create a new attempt if there is no unfinished one -/// Check number of attempts - $numberofpreviousattempts = count_records_select('quiz_attempts', "quiz = '{$quiz->id}' AND " . - "userid = '{$USER->id}' AND timefinish > 0 AND preview != 1"); - if ($quiz->attempts and $numberofpreviousattempts >= $quiz->attempts) { - error(get_string('nomoreattempts', 'quiz'), "view.php?id={$cm->id}"); +/// Check to see if a new preview was requested. + if ($canpreview && $forcenew) { + /// Teacher wants a new preview, so we set a finish time on the + /// current attempt (if any). It will then automatically be deleted below + set_field('quiz_attempts', 'timefinish', $timenow, 'quiz', $quiz->id, 'userid', $USER->id); } -/// Check subnet access - if (!$ispreviewing && $quiz->subnet && !address_in_subnet(getremoteaddr(), $quiz->subnet)) { - error(get_string("subneterror", "quiz"), "view.php?id=$cm->id"); - } +/// Look for an existing attempt. + $newattempt = false; + $lastattempt = quiz_get_latest_attempt_by_user($quiz->id, $USER->id); + + if ($lastattempt && !$lastattempt->timefinish) { + /// Continuation of an attempt. + $attempt = $lastattempt; + $lastattemptid = false; + + /// Log it, but only if some time has elapsed. + if (($timenow - $attempt->timemodified) > QUIZ_CONTINUE_ATTEMPT_LOG_INTERVAL) { + /// This action used to be 'continue attempt' but the database field has only 15 characters. + add_to_log($course->id, 'quiz', 'continue attemp', "review.php?attempt=$attempt->id", + "$quiz->id", $cm->id); + } -/// Check password access - if ($ispreviewing && $forcenew) { - unset($SESSION->passwordcheckedquizzes[$quiz->id]); - } + } else { + /// Start a new attempt. + $newattempt = true; - if ($quiz->password and empty($SESSION->passwordcheckedquizzes[$quiz->id])) { - $enteredpassword = optional_param('quizpassword', '', PARAM_RAW); - if (optional_param('cancelpassword', false)) { - // User clicked cancel in the password form. - redirect($CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id); - } else if (strcmp($quiz->password, $enteredpassword) === 0) { - // User entered the correct password. - $SESSION->passwordcheckedquizzes[$quiz->id] = true; + /// Get number for the next or unfinished attempt + if ($lastattempt && !$lastattempt->preview && !$canpreview) { + $attemptnumber = $lastattempt->attempt + 1; + $lastattemptid = $lastattempt->id; } else { - // User entered the wrong password, or has not entered one yet. - $url = $CFG->wwwroot . '/mod/quiz/attempt.php?q=' . $quiz->id; - - if (empty($popup)) { - print_header('', '', '', 'quizpassword'); - } - - if (trim(strip_tags($quiz->intro))) { - $formatoptions->noclean = true; - print_box(format_text($quiz->intro, FORMAT_MOODLE, $formatoptions), 'generalbox', 'intro'); - } - print_box_start('generalbox', 'passwordbox'); - if (!empty($enteredpassword)) { - echo '

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

'; - } -?> -

-
-
- - - - -
-
-delay1 or $quiz->delay2) { - //quiz enforced time delay - if ($attempts = quiz_get_user_attempts($quiz->id, $USER->id)) { - $numattempts = count($attempts); - } else { - $numattempts = 0; - } - $timenow = time(); - $lastattempt_obj = get_record_select('quiz_attempts', "quiz = $quiz->id AND attempt = $numattempts AND userid = $USER->id", 'timefinish'); - if ($lastattempt_obj) { - $lastattempt = $lastattempt_obj->timefinish; - } - if ($numattempts == 1 && $quiz->delay1) { - if ($timenow - $quiz->delay1 < $lastattempt) { - error(get_string('timedelay', 'quiz'), 'view.php?q='.$quiz->id); - } - } else if($numattempts > 1 && $quiz->delay2) { - if ($timenow - $quiz->delay2 < $lastattempt) { - error(get_string('timedelay', 'quiz'), 'view.php?q='.$quiz->id); - } + /// Check access. + $messages = $accessmanager->prevent_access() + + $accessmanager->prevent_new_attempt($attemptnumber - 1, $lastattempt); + if (!$canpreview && $messages) { + error($accessmanager->print_messages($messages, true), + $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id); } - } - -/// Load attempt or create a new attempt if there is no unfinished one + $accessmanager->do_password_check($canpreview); - if ($ispreviewing and $forcenew) { // teacher wants a new preview - // so we set a finish time on the current attempt (if any). - // It will then automatically be deleted below - set_field('quiz_attempts', 'timefinish', $timestamp, 'quiz', $quiz->id, 'userid', $USER->id); - } - - $attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id); - - $newattempt = false; - if (!$attempt) { - // Delete any previous preview attempts belonging to this user. + /// Delete any previous preview attempts belonging to this user. if ($oldattempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND userid = '$USER->id' AND preview = 1")) { foreach ($oldattempts as $oldattempt) { quiz_delete_attempt($oldattempt, $quiz); } } - $newattempt = true; - // Start a new attempt and initialize the question sessions - $attempt = quiz_create_attempt($quiz, $attemptnumber); - // If this is an attempt by a teacher mark it as a preview - if ($ispreviewing) { - $attempt->preview = 1; - } - // Save the attempt + + /// Create the new attempt and initialize the question sessions + $attempt = quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $canpreview); + + /// Save the attempt in the database. if (!$attempt->id = insert_record('quiz_attempts', $attempt)) { - error('Could not create new attempt'); + quiz_error($quiz, 'newattemptfail'); } - // make log entries - if ($ispreviewing) { - add_to_log($course->id, 'quiz', 'preview', - "attempt.php?id=$cm->id", - "$quiz->id", $cm->id); + + /// Log the new attempt. + if ($attempt->preview) { + add_to_log($course->id, 'quiz', 'preview', "attempt.php?id=$cm->id", + "$quiz->id", $cm->id); } else { - add_to_log($course->id, 'quiz', 'attempt', - "review.php?attempt=$attempt->id", - "$quiz->id", $cm->id); - } - } else { - // log continuation of attempt only if some time has lapsed - if (($timestamp - $attempt->timemodified) > 600) { // 10 minutes have elapsed - add_to_log($course->id, 'quiz', 'continue attemp', // this action used to be called 'continue attempt' but the database field has only 15 characters - "review.php?attempt=$attempt->id", - "$quiz->id", $cm->id); + add_to_log($course->id, 'quiz', 'attempt', "review.php?attempt=$attempt->id", + "$quiz->id", $cm->id); } } - if (!$attempt->timestart) { // shouldn't really happen, just for robustness + +/// This shouldn't really happen, just for robustness + if (!$attempt->timestart) { debugging('timestart was not set for this attempt. That should be impossible.', DEBUG_DEVELOPER); - $attempt->timestart = $timestamp - 1; + $attempt->timestart = $timenow - 1; } /// Load all the questions and states needed by this script - // list of questions needed by page +/// Get the list of questions needed by this page. $pagelist = quiz_questions_on_page($attempt->layout, $page); - if ($newattempt) { + if ($newattempt || $finishattempt) { $questionlist = quiz_questions_in_quiz($attempt->layout); } else { $questionlist = $pagelist; } - // add all questions that are on the submitted form +/// Add all questions that are on the submitted form if ($questionids) { $questionlist .= ','.$questionids; } if (!$questionlist) { - error(get_string('noquestionsfound', 'quiz'), 'view.php?q='.$quiz->id); - } - - $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". - " FROM {$CFG->prefix}question q,". - " {$CFG->prefix}quiz_question_instances i". - " WHERE i.quiz = '$quiz->id' AND q.id = i.question". - " AND q.id IN ($questionlist)"; - - // Load the questions - if (!$questions = get_records_sql($sql)) { - error(get_string('noquestionsfound', 'quiz'), 'view.php?q='.$quiz->id); + quiz_error($quiz, 'noquestionsfound'); } - // Load the question type specific information - if (!get_question_options($questions)) { - error('Could not load question options'); + $questions = question_load_questions($questionlist, 'qqi.grade AS maxgrade, qqi.id AS instance', + 'quiz_question_instances qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); + if (is_string($questions)) { + quiz_error($quiz, 'loadingquestionsfailed', $questions); } - // If the new attempt is to be based on a previous attempt find its id - $lastattemptid = false; - if ($newattempt and $attempt->attempt > 1 and $quiz->attemptonlast and !$attempt->preview) { - // Find the previous attempt - if (!$lastattemptid = get_field('quiz_attempts', 'uniqueid', 'quiz', $attempt->quiz, 'userid', $attempt->userid, 'attempt', $attempt->attempt-1)) { - error('Could not find previous attempt to build on'); - } - } - - // Restore the question sessions to their most recent states - // creating new sessions where required +/// Restore the question sessions to their most recent states creating new sessions where required. if (!$states = get_question_states($questions, $quiz, $attempt, $lastattemptid)) { error('Could not restore question sessions'); } - // Save all the newly created states +/// If we are starting a new attempt, save all the newly created states. if ($newattempt) { foreach ($questions as $i => $question) { save_question_session($questions[$i], $states[$i]); @@ -277,14 +198,18 @@ } /// Process form data ///////////////////////////////////////////////// - if ($responses = data_submitted() and empty($_POST['quizpassword'])) { - // set the default event. This can be overruled by individual buttons. - $event = (array_key_exists('markall', $responses)) ? QUESTION_EVENTSUBMIT : - ($finishattempt ? QUESTION_EVENTCLOSE : QUESTION_EVENTSAVE); + /// Set the default event. This can be overruled by individual buttons. + if (array_key_exists('markall', $responses)) { + $event = QUESTION_EVENTSUBMIT; + } else if ($finishattempt) { + $event = QUESTION_EVENTCLOSE; + } else { + $event = QUESTION_EVENTSAVE; + } - // Unset any variables we know are not responses + /// Unset any variables we know are not responses unset($responses->id); unset($responses->q); unset($responses->oldpage); @@ -296,93 +221,74 @@ unset($responses->markall); unset($responses->forcenewattempt); - // extract responses - // $actions is an array indexed by the questions ids + /// Extract the responses. $actions will be an array indexed by the questions ids. $actions = question_extract_responses($questions, $responses, $event); - // Process each question in turn - + /// Process each question in turn $questionidarray = explode(',', $questionids); foreach($questionidarray as $i) { if (!isset($actions[$i])) { $actions[$i]->responses = array('' => ''); $actions[$i]->event = QUESTION_EVENTOPEN; } - $actions[$i]->timestamp = $timestamp; + $actions[$i]->timestamp = $timenow; question_process_responses($questions[$i], $states[$i], $actions[$i], $quiz, $attempt); save_question_session($questions[$i], $states[$i]); } - $attempt->timemodified = $timestamp; - - // We have now finished processing form data + $attempt->timemodified = $timenow; + if (!update_record('quiz_attempts', $attempt)) { + quiz_error($quiz, 'saveattemptfailed'); + } } /// Finish attempt if requested if ($finishattempt) { - // Set the attempt to be finished - $attempt->timefinish = $timestamp; - - // load all the questions - $closequestionlist = quiz_questions_in_quiz($attempt->layout); - $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". - " FROM {$CFG->prefix}question q,". - " {$CFG->prefix}quiz_question_instances i". - " WHERE i.quiz = '$quiz->id' AND q.id = i.question". - " AND q.id IN ($closequestionlist)"; - if (!$closequestions = get_records_sql($sql)) { - error('Questions missing'); - } + /// Set the attempt to be finished + $attempt->timefinish = $timenow; - // Load the question type specific information - if (!get_question_options($closequestions)) { - error('Could not load question options'); - } - - // Restore the question sessions - if (!$closestates = get_question_states($closequestions, $quiz, $attempt)) { - error('Could not restore question sessions'); - } - - foreach($closequestions as $key => $question) { + /// Move each question to the closed state. + foreach ($questions as $key => $question) { $action->event = QUESTION_EVENTCLOSE; - $action->responses = $closestates[$key]->responses; - $action->timestamp = $closestates[$key]->timestamp; - question_process_responses($question, $closestates[$key], $action, $quiz, $attempt); - save_question_session($question, $closestates[$key]); + $action->responses = $states[$key]->responses; + $action->timestamp = $states[$key]->timestamp; + question_process_responses($question, $states[$key], $action, $quiz, $attempt); + save_question_session($question, $states[$key]); } - add_to_log($course->id, 'quiz', 'close attempt', - "review.php?attempt=$attempt->id", - "$quiz->id", $cm->id); - } + /// Log the end of this attempt. + add_to_log($course->id, 'quiz', 'close attempt', "review.php?attempt=$attempt->id", + "$quiz->id", $cm->id); -/// Update the quiz attempt and the overall grade for the quiz - if ($responses || $finishattempt) { + /// Update the quiz attempt record. if (!update_record('quiz_attempts', $attempt)) { - error('Failed to save the current quiz attempt!'); + quiz_error($quiz, 'saveattemptfailed'); } - if (($attempt->attempt > 1 || $attempt->timefinish > 0) and !$attempt->preview) { + + if (!$attempt->preview) { + /// Record this user's best grade (if this is not a preview). quiz_save_best_grade($quiz); + + /// Send any notification emails (if this is not a preview). + quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm); } - } -/// Send emails to those who have the capability set - if ($finishattempt && !$attempt->preview) { - quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm); - } + /// Clear the password check flag in the session. + $accessmanager->clear_password_access(); - if ($finishattempt) { - if (!empty($SESSION->passwordcheckedquizzes[$quiz->id])) { - unset($SESSION->passwordcheckedquizzes[$quiz->id]); - } + /// Send the user to the review page. redirect($CFG->wwwroot . '/mod/quiz/review.php?attempt='.$attempt->id, 0); } -// Now is the right time to check the open and close times. - if (!$ispreviewing && ($timestamp < $quiz->timeopen || ($quiz->timeclose && $timestamp > $quiz->timeclose))) { - error(get_string('notavailable', 'quiz'), "view.php?id={$cm->id}"); +/// Now is the right time to check access (unless we are starting a new attempt, and did it above). + if (!$newattempt) { + $messages = $accessmanager->prevent_access(); + if (!$canpreview && $messages) { + error($accessmanager->print_messages($messages, true), + $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id); + } + $accessmanager->do_password_check($canpreview); } /// Print the quiz page //////////////////////////////////////////////////////// @@ -390,11 +296,10 @@ // Print the page header require_js($CFG->wwwroot . '/mod/quiz/quiz.js'); $pagequestions = explode(',', $pagelist); + $strattemptnum = get_string('attempt', 'quiz', $attempt->attempt); $headtags = get_html_head_contributions($pagequestions, $questions, $states); - if (!empty($popup)) { - define('MESSAGE_WINDOW', true); // This prevents the message window coming up - print_header($course->shortname.': '.format_string($quiz->name), '', '', '', $headtags, false, '', '', false, ''); - include('protect_js.php'); + if ($accessmanager->securewindow_required($canpreview)) { + $accessmanager->setup_secure_page($course->shortname.': '.format_string($quiz->name), $headtags); } else { $strupdatemodule = has_capability('moodle/course:manageactivities', $coursecontext) ? update_module_button($cm->id, $course->id, get_string('modulename', 'quiz')) @@ -402,32 +307,26 @@ $navigation = build_navigation($strattemptnum, $cm); print_header_simple(format_string($quiz->name), "", $navigation, "", $headtags, true, $strupdatemodule); } - echo ''; // for overlib - // Print the quiz name heading and tabs for teacher, etc. - if ($ispreviewing) { + if ($canpreview) { + /// Show the tab bar. $currenttab = 'preview'; include('tabs.php'); + /// Heading and tab bar. print_heading(get_string('previewquiz', 'quiz', format_string($quiz->name))); - unset($buttonoptions); - $buttonoptions['q'] = $quiz->id; - $buttonoptions['forcenew'] = true; - echo '
'; - print_single_button($CFG->wwwroot.'/mod/quiz/attempt.php', $buttonoptions, get_string('startagain', 'quiz')); - echo '
'; - /// Notices about restrictions that would affect students. - if ($quiz->popup) { - notify(get_string('popupnotice', 'quiz')); - } - if ($timestamp < $quiz->timeopen || ($quiz->timeclose && $timestamp > $quiz->timeclose)) { - notify(get_string('notavailabletostudents', 'quiz')); - } - if ($quiz->subnet && !address_in_subnet(getremoteaddr(), $quiz->subnet)) { - notify(get_string('subnetnotice', 'quiz')); + print_restart_preview_button($quiz); + + /// Inform teachers of any restrictions that would apply to students at this point. + if ($messages) { + print_box_start('quizaccessnotices'); + print_heading(get_string('accessnoticesheader', 'quiz'), '', 3); + $accessmanager->print_messages($messages); + print_box_end(); } } else { + /// Just a heading. if ($quiz->attempts != 1) { print_heading(format_string($quiz->name).' - '.$strattemptnum); } else { @@ -492,31 +391,18 @@ echo ''; echo ''; - // Add a hidden field with questionids. Do this at the end of the form, so + // Add a hidden field with questionids. Do this at the end of the form, so // if you navigate before the form has finished loading, it does not wipe all // the student's answers. echo '\n"; echo "\n"; - // If the quiz has a time limit, or if we are close to the close time, include a floating timer. - $showtimer = false; - $timerstartvalue = 999999999999; - if ($quiz->timeclose) { - $timerstartvalue = min($timerstartvalue, $quiz->timeclose - time()); - $showtimer = $timerstartvalue < 60*60; // Show the timer if we are less than 60 mins from the deadline. - } - if ($quiz->timelimit > 0 && !has_capability('mod/quiz:ignoretimelimits', $context, NULL, false)) { - $timerstartvalue = min($timerstartvalue, $attempt->timestart + $quiz->timelimit*60- time()); - $showtimer = true; - } - if ($showtimer && (!$ispreviewing || $timerstartvalue > 0)) { - $timerstartvalue = max($timerstartvalue, 1); // Make sure it starts just above zero. - require('jstimer.php'); - } - // Finish the page - if (empty($popup)) { + $accessmanager->show_attempt_timer_if_needed($attempt, time()); + if ($accessmanager->securewindow_required($canpreview)) { + print_footer('empty'); + } else { print_footer($course); } ?> diff --git a/mod/quiz/attempt_close_js.php b/mod/quiz/attempt_close_js.php deleted file mode 100644 index 3ba2d5b6dc..0000000000 --- a/mod/quiz/attempt_close_js.php +++ /dev/null @@ -1,27 +0,0 @@ - - -
- - - - - - $cm->id ), get_string('finishreview', 'quiz')); -} -?> -
diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index aaf50d8906..a48c4fcf8b 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -20,6 +20,7 @@ * Include those library functions that are also used by core Moodle or other modules */ require_once($CFG->dirroot . '/mod/quiz/lib.php'); +require_once($CFG->dirroot . '/mod/quiz/accessrules.php'); require_once($CFG->dirroot . '/question/editlib.php'); /// Constants /////////////////////////////////////////////////////////////////// @@ -37,13 +38,25 @@ define("QUIZ_ATTEMPTLAST", "4"); /**#@+ * Constants to describe the various states a quiz attempt can be in. */ -define('QUIZ_STATE_DURING', 'during'); -define('QUIZ_STATE_IMMEDIATELY', 'immedately'); -define('QUIZ_STATE_OPEN', 'open'); -define('QUIZ_STATE_CLOSED', 'closed'); +define('QUIZ_STATE_DURING', 'during'); +define('QUIZ_STATE_IMMEDIATELY', 'immedately'); +define('QUIZ_STATE_OPEN', 'open'); +define('QUIZ_STATE_CLOSED', 'closed'); define('QUIZ_STATE_TEACHERACCESS', 'teacheraccess'); // State only relevant if you are in a studenty role. /**#@-*/ +/** + * We don't log every single hit on attempt.php, only significant ones like starting and + * ending an attempt, and periodically during the attempt, as defined by this constant. (10 mins) + */ +define('QUIZ_CONTINUE_ATTEMPT_LOG_INTERVAL', '600'); + +/** + * We show the countdown timer if there is less than this amount of time left before the + * the quiz close date. (1 hour) + */ +define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600'); + /// Functions related to attempts ///////////////////////////////////////// /** @@ -52,15 +65,22 @@ define('QUIZ_STATE_TEACHERACCESS', 'teacheraccess'); // State only relevant if y * Creates an attempt object to represent an attempt at the quiz by the current * user starting at the current time. The ->id field is not set. The object is * NOT written to the database. - * @return object The newly created attempt object. - * @param object $quiz The quiz to create an attempt for. - * @param integer $attemptnumber The sequence number for the attempt. + * + * @param object $quiz the quiz to create an attempt for. + * @param integer $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. + * @param integer $timenow the time the attempt was started at. + * @param boolean $ispreview whether this new attempt is a preview. + * + * @return object the newly created attempt object. */ -function quiz_create_attempt($quiz, $attemptnumber) { - global $USER, $CFG; +function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) { + global $USER; - if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) { - // we are not building on last attempt so create a new attempt + if ($attemptnumber = 1 || !$quiz->attemptonlast) { + /// We are not building on last attempt so create a new attempt. + $attempt = new stdClass; $attempt->quiz = $quiz->id; $attempt->userid = $USER->id; $attempt->preview = 0; @@ -69,9 +89,14 @@ function quiz_create_attempt($quiz, $attemptnumber) { } else { $attempt->layout = $quiz->questions; } + } else { + /// Build on last attempt. + if (empty($lastattempt)) { + error(get_string('cannotfindprevattempt', 'quiz')); + } + $attempt = $lastattempt; } - $timenow = time(); $attempt->attempt = $attemptnumber; $attempt->sumgrades = 0.0; $attempt->timestart = $timenow; @@ -79,12 +104,17 @@ function quiz_create_attempt($quiz, $attemptnumber) { $attempt->timemodified = $timenow; $attempt->uniqueid = question_new_attempt_uniqueid(); +/// If this is a preview, mark it as such. + if ($ispreview) { + $attempt->preview = 1; + } + return $attempt; } /** - * Returns an unfinished attempt (if there is one) for the given - * user on the given quiz. This function does not return preview attempts. + * Returns the unfinished attempt for the given + * user on the given quiz, if there is one. * * @param integer $quizid the id of the quiz. * @param integer $userid the id of the user. @@ -100,6 +130,41 @@ function quiz_get_user_attempt_unfinished($quizid, $userid) { } } +/** + * Returns the most recent attempt by a given user on a given quiz. + * May be finished, or may not. + * + * @param integer $quizid the id of the quiz. + * @param integer $userid the id of the user. + * + * @return mixed the unfinished attempt if there is one, false if not. + */ +function quiz_get_latest_attempt_by_user($quizid, $userid) { + global $CFG; + return get_record_sql('SELECT qa.* FROM ' . $CFG->prefix . 'quiz_attempts qa + WHERE qa.quiz=' . $quizid . ' AND qa.userid=' . $userid . ' AND qa.timestart = ( + SELECT MAX(timestart) FROM ' . $CFG->prefix . 'quiz_attempts ssqa + WHERE ssqa.quiz=' . $quizid . ' AND ssqa.userid=' . $userid . ')'); +} + +/** + * Load an attempt by id. You need to use this method instead of get_record, because + * of some ancient history to do with the upgrade from Moodle 1.4 to 1.5, See the comment + * after CREATE TABLE `prefix_quiz_newest_states` in mod/quiz/db/mysql.php. + * + * @param integer $attemptid the id of the attempt to load. + */ +function quiz_load_attempt($attemptid) { + $attempt = get_record('quiz_attempts', 'id', $attemptid); + + if (!record_exists('question_sessions', 'attemptid', $attempt->uniqueid)) { + /// this attempt has not yet been upgraded to the new model + quiz_upgrade_states($attempt); + } + + return $attempt; +} + /** * Delete a quiz attempt. */ @@ -698,12 +763,12 @@ function quiz_get_reviewoptions($quiz, $attempt, $context=null) { $options->questioncommentlink = '/mod/quiz/comment.php'; } - if (!is_null($context) && has_capability('mod/quiz:viewreports', $context) && + if (!is_null($context) && has_capability('mod/quiz:viewreports', $context) && has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) { // People who can see reports and hidden grades should be shown everything, // except during preview when teachers want to see what students see. $options->responses = true; - $options->scores = true; + $options->scores = true; $options->feedback = true; $options->correct_responses = true; $options->solutions = false; @@ -723,7 +788,7 @@ function quiz_get_reviewoptions($quiz, $attempt, $context=null) { $options->quizstate = QUIZ_STATE_CLOSED; } - // ... and hence extract the appropriate review options. + // ... and hence extract the appropriate review options. $options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0; $options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0; $options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0; @@ -769,6 +834,14 @@ function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) { return array($someoptions, $alloptions); } +function print_restart_preview_button($quiz) { + global $CFG; + echo '
'; + print_single_button($CFG->wwwroot . '/mod/quiz/attempt.php', + array('q' => $quiz->id, 'forcenew' => true), get_string('startagain', 'quiz')); + echo '
'; +} + /// FUNCTIONS FOR SENDING NOTIFICATION EMAILS /////////////////////////////// /** @@ -938,4 +1011,19 @@ function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) // return the number of successfully sent emails return $emailresult['good']; } + +/** + * Print a quiz error message. This is a thin wrapper around print_error, for convinience. + * + * @param mixed $quiz either the quiz object, or the interger quiz id. + * @param string $errorcode the name of the string from quiz.php to print. + * @param object $a any extra data required by the error string. + */ +function quiz_error($quiz, $errorcode, $a = null) { + global $CFG; + if (is_object($quiz)) { + $quiz = $quiz->id; + } + print_error($errorcode, 'quiz', $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz, $a); +} ?> diff --git a/mod/quiz/quiz.js b/mod/quiz/quiz.js index a39c1e935f..37ce19afa4 100644 --- a/mod/quiz/quiz.js +++ b/mod/quiz/quiz.js @@ -16,7 +16,7 @@ function navigate(page) { ourForm.submit(); } -/* Use this in an onkeypress handler, to stop enter submitting the forum unless you +/* Use this in an onkeypress handler, to stop enter submitting the forum unless you are actually on the submit button. Don't stop the user typing things in text areas. */ function check_enter(e) { var target = e.target ? e.target : e.srcElement; @@ -28,65 +28,218 @@ function check_enter(e) { return true; } -/* Used to update the on-screen countdown clock for quizzes with a time limit */ -function countdown_clock(theTimer) { - var timeout_id = null; +quiz_timer = { + // The outer div, so we can get at it to move it when the page scrolls. + timerouter: null, - quizTimerValue = Math.floor((ec_quiz_finish - new Date().getTime())/1000); + // The element that the time should be displayed in. + timerdisplay: null, - if(quizTimerValue <= 0) { - clearTimeout(timeout_id); - document.getElementById('timeup').value = 1; - var ourForm = document.getElementById('responseform'); - if (ourForm.onsubmit) { - ourForm.onsubmit(); + // The main quiz for, which we will need to submit when the time expires. + quizform: null, + + // String that is displayed after the time has run out. + strtimeup: '', + + // How long is left, in seconds. + endtime: 0, + + // How often we update the clock display. Delay in milliseconds. + updatedelay: 500, + + // This records the id of the timeout that updates the clock periodically, so we can cancel it + // Once time has run out. + timeoutid: null, + + // Desired position of the top of timer_outer: 100px from the top of the window. + targettop: 100, + + // How often we check to positing and adjust it. Delay in milliseconds. + movedelay: 100, + + // Last known postion of timer_outer. + oldtop: this.target_top, + + // Colours used to change the timer bacground colour when time had nearly run out. + // This array is indexed by number of seconds left. + finalcolours: [ + '#ff0000', + '#ff1111', + '#ff2222', + '#ff3333', + '#ff4444', + '#ff5555', + '#ff6666', + '#ff7777', + '#ff8888', + '#ff9999', + '#ffaaaa', + '#ffbbbb', + '#ffcccc', + '#ffdddd', + '#ffeeee', + '#ffffff', + ], + + // Initialise method. + initialise: function(strtimeup, timeleft) { + // Set some fields. + quiz_timer.strtimeup = strtimeup; + quiz_timer.endtime = new Date().getTime() + timeleft*1000; + + // Get references to some bits of the DOM we need. + quiz_timer.timerouter = document.getElementById('quiz-timer-outer'), + quiz_timer.timerdisplay = document.getElementById('quiz-timer-display'), + quiz_timer.quizform = document.getElementById('responseform'), + + // Get things starte. + quiz_timer.move(); + quiz_timer.update_time(); + }, + + // Stop method. Stops the timer if it is running. + stop: function() { + if (quiz_timer.timeoutid) { + clearTimeout(quiz_timer.timeoutid); } - ourForm.submit(); - return; - } + }, - now = quizTimerValue; - var hours = Math.floor(now/3600); - now = now - (hours*3600); - var minutes = Math.floor(now/60); - now = now - (minutes*60); - var seconds = now; - - var t = "" + hours; - t += ((minutes < 10) ? ":0" : ":") + minutes; - t += ((seconds < 10) ? ":0" : ":") + seconds; - window.status = t.toString(); - - if(hours == 0 && minutes == 0 && seconds <= 15) { - //go from fff0f0 to ffe0e0 to ffd0d0...ff2020, ff1010, ff0000 in 15 steps - var hexascii = "0123456789ABCDEF"; - var col = '#' + 'ff' + hexascii.charAt(seconds) + '0' + hexascii.charAt(seconds) + 0; - theTimer.style.backgroundColor = col; - } - document.getElementById('time').value = t.toString(); - timeout_id = setTimeout("countdown_clock(theTimer)", 1000); -} + // Function that updates the text displayed in element timer_display. + set_displayed_time: function(str) { + var display = quiz_timer.timerdisplay + if (!display.firstChild) { + display.appendChild(document.createTextNode(str)) + } else if (display.firstChild.nodeType == 3) { + display.firstChild.replaceData(0, display.firstChild.length, str); + } else { + display.replaceChild(document.createTextNode(str), display.firstChild); + } + }, -/* Use to keep the quiz timer on-screen as the user scrolls. */ -function movecounter(timerbox) { - var pos; + // Function to convert a number between 0 and 99 to a two-digit string. + two_digit: function(num) { + if (num < 10) { + return '0' + num; + } else { + return num; + } + }, - if (window.innerHeight) { - pos = window.pageYOffset - } else if (document.documentElement && document.documentElement.scrollTop) { - pos = document.documentElement.scrollTop - } else if (document.body) { - pos = document.body.scrollTop - } + // Function to update the clock with the current time left, and submit the quiz if necessary. + update_time: function() { + var secondsleft = Math.floor((quiz_timer.endtime - new Date().getTime())/1000); - if (pos < theTop) { - pos = theTop; - } else { - pos += 100; + // If time has expired, Set the hidden form field that says time has expired. + if (secondsleft < 0) { + quiz_timer.stop(); + quiz_timer.set_displayed_time(quiz_timer.strtimeup); + document.getElementById('timeup').value = 1; + if (quiz_timer.quizform.onsubmit) { + quiz_timer.quizform.onsubmit(); + } + quiz_timer.quizform.submit(); + return; + } + + // If time has nearly expired, change the colour. + if (secondsleft < quiz_timer.finalcolours.length) { + quiz_timer.timerouter.style.backgroundColor = quiz_timer.finalcolours[secondsleft]; + } + + // Update the time display. + var hours = Math.floor(secondsleft/3600); + secondsleft -= hours*3600; + var minutes = Math.floor(secondsleft/60); + secondsleft -= minutes*60; + var seconds = secondsleft; + quiz_timer.set_displayed_time('' + hours + ':' + quiz_timer.two_digit(minutes) + ':' + + quiz_timer.two_digit(seconds)); + + // Arrange for this method to be called again soon. + quiz_timer.timeoutid = setTimeout(quiz_timer.update_time, quiz_timer.updatedelay); + }, + + // Function to keep the clock in the same place on the screen. + move: function() { + // Work out where the top of the window is. + var pos; + if (window.innerHeight) { + pos = window.pageYOffset + } else if (document.documentElement && document.documentElement.scrollTop) { + pos = document.documentElement.scrollTop + } else if (document.body) { + pos = document.body.scrollTop + } + + // We want the timer target_top pixels from the top of the window, + // or the top of the document, whichever is lower. + pos += quiz_timer.targettop; + if (pos < quiz_timer.targettop) { + pos = quiz_timer.targettop; + } + + // Only move the timer if the window has stopped moving, and the position has stabilised. + if (pos == quiz_timer.oldtop) { + quiz_timer.timerouter.style.top = pos + 'px'; + } + quiz_timer.oldtop = pos; + + // Arrange for this method to be called again soon. + setTimeout(quiz_timer.move, quiz_timer.movedelay); } - if (pos == old) { - timerbox.style.top = pos + 'px'; +}; + +quiz_secure_window = { + // The message displayed when the secure window interferes with the user. + protection_message: null, + + // Used by close. The URL to redirect to, if we find we are not acutally in a pop-up window. + close_next_url: '', + + // Code for secure window. This used to be in protect_js.php. I don't understand it, + // I have just moved it for clenliness reasons. + initialise: function(strmessage) { + quiz_secure_window.protection_message = strmessage; + if (document.layers) { + document.captureEvents(Event.MOUSEDOWN); + } + document.onmousedown = quiz_secure_window.intercept_click; + document.oncontextmenu = new Function("alert(quiz_secure_window.protection_message); return false") + }, + + // Code for secure window. This used to be in protect_js.php. I don't understand it, + // I have just moved it for clenliness reasons. + intercept_click: function(e) { + if (document.all) { + if (event.button==1) { + return false; + } + if (event.button==2) { + alert(quiz_securewindow_message); + return false; + } + } + if (document.layers) { + if (e.which > 1) { + alert(quiz_securewindow_message); + return false; + } + } + }, + + close: function(url, delay) { + if (url != '') { + quiz_secure_window.close_next_url = url; + } + if (delay > 0) { + setTimeout('quiz_close_securewindow("", 0)', delay*1000); + } else { + if (window.opener) { + window.opener.document.location.reload(); + window.close(); + } else if (quiz_secure_window.close_next_url != '') { + window.location.href = quiz_secure_window.close_next_url; + } + } } - old = pos; - temp = setTimeout('movecounter(timerbox)',100); -} +}; \ No newline at end of file diff --git a/mod/quiz/review.php b/mod/quiz/review.php index 0ca3c71539..15a9f15f95 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -10,14 +10,14 @@ * @package quiz */ - require_once("../../config.php"); - require_once("locallib.php"); + require_once('../../config.php'); + require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - $attempt = required_param('attempt', PARAM_INT); // A particular attempt ID for review + $attemptid = required_param('attempt', PARAM_INT); // A particular attempt ID for review $page = optional_param('page', 0, PARAM_INT); // The required page $showall = optional_param('showall', 0, PARAM_BOOL); - if (! $attempt = get_record("quiz_attempts", "id", $attempt)) { + if (!$attempt = quiz_load_attempt($attemptid)) { error("No such attempt ID exists"); } if (! $quiz = get_record("quiz", "id", $attempt->quiz)) { @@ -30,102 +30,74 @@ error("The course module for the quiz with id $quiz->id is missing"); } - $grade = quiz_rescale_grade($attempt->sumgrades, $quiz); - $feedback = quiz_feedback_for_grade($grade, $attempt->quiz); - - if (!count_records('question_sessions', 'attemptid', $attempt->uniqueid)) { - // this question has not yet been upgraded to the new model - quiz_upgrade_states($attempt); - } - +/// Check login and get contexts. require_login($course->id, false, $cm); - $context = get_context_instance(CONTEXT_MODULE, $cm->id); $coursecontext = get_context_instance(CONTEXT_COURSE, $cm->course); - $isteacher = has_capability('mod/quiz:preview', get_context_instance(CONTEXT_MODULE, $cm->id)); - $options = quiz_get_reviewoptions($quiz, $attempt, $context); - $popup = $isteacher ? 0 : $quiz->popup; // Controls whether this is shown in a javascript-protected window. + $context = get_context_instance(CONTEXT_MODULE, $cm->id); + $canpreview = has_capability('mod/quiz:preview', get_context_instance(CONTEXT_MODULE, $cm->id)); +/// Create an object to manage all the other (non-roles) access rules. $timenow = time(); + $accessmanager = new quiz_access_manager($quiz, $timenow, + has_capability('mod/quiz:ignoretimelimits', $context, NULL, false)); + $options = quiz_get_reviewoptions($quiz, $attempt, $context); + +/// Work out if this is a student viewing their own attempt/teacher previewing, +/// or someone with 'mod/quiz:viewreports' reviewing someone elses attempt. + $reviewofownattempt = $attempt->userid == $USER->id && (!$canpreview || $attempt->preview); + +/// Permissions checks for normal users who do not have quiz:viewreports capability. if (!has_capability('mod/quiz:viewreports', $context)) { - // Can't review during the attempt. + /// Can't review during the attempt - send them back to the attempt page. if (!$attempt->timefinish) { - redirect('attempt.php?q=' . $quiz->id); + redirect($CFG->wwwroot . '/mod/quiz/attempt.php?q=' . $quiz->id); } - // Can't review other student's attempts. - if ($attempt->userid != $USER->id) { - error("This is not your attempt!", 'view.php?q=' . $quiz->id); + if ($messages = $accessmanager->prevent_review($options)) { + } - // Can't review if Student's may review ... Responses is turned on. + /// Can't review other users' attempts. + if (!$reviewofownattempt) { + quiz_error($quiz, 'reviewnotallowed'); + } + /// Can't review unless Students may review -> Responses option is turned on. if (!$options->responses) { - if ($options->quizstate == QUIZ_STATE_IMMEDIATELY) { - $message = ''; - } else if ($options->quizstate == QUIZ_STATE_OPEN && $quiz->timeclose && - ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) { - $message = get_string('noreviewuntil', 'quiz', userdate($quiz->timeclose)); - } else { - $message = get_string('noreview', 'quiz'); - } - if (empty($popup)) { - redirect('view.php?q=' . $quiz->id, $message); - } else { - ?>back_to_view_page($canpreview, + $accessmanager->cannot_review_message($options)); } } +/// Log this review. add_to_log($course->id, "quiz", "review", "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", "$cm->id"); -/// Load all the questions and states needed by this script - - // load the questions needed by page - $pagelist = $showall ? quiz_questions_in_quiz($attempt->layout) : quiz_questions_on_page($attempt->layout, $page); - $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". - " FROM {$CFG->prefix}question q,". - " {$CFG->prefix}quiz_question_instances i". - " WHERE i.quiz = '$quiz->id' AND q.id = i.question". - " AND q.id IN ($pagelist)"; - if (!$questions = get_records_sql($sql)) { - error('No questions found'); +/// load the questions needed by page + if ($showall) { + $questionlist = quiz_questions_in_quiz($attempt->layout); + } else { + $questionlist = quiz_questions_on_page($attempt->layout, $page); } - - // Load the question type specific information - if (!get_question_options($questions)) { - error('Could not load question options'); + $pagequestions = explode(',', $questionlist); + $questions = question_load_questions($questionlist, 'qqi.grade AS maxgrade, qqi.id AS instance', + 'quiz_question_instances qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); + if (is_string($questions)) { + quiz_error($quiz, 'loadingquestionsfailed', $questions); } - // Restore the question sessions to their most recent states - // creating new sessions where required +/// Restore the question sessions to their most recent states creating new sessions where required. if (!$states = get_question_states($questions, $quiz, $attempt)) { error('Could not restore question sessions'); } -/// Print the page header - - $strscore = get_string("score", "quiz"); - $strgrade = get_string("grade"); - $strbestgrade = get_string("bestgrade", "quiz"); - $strtimetaken = get_string("timetaken", "quiz"); - $strtimecompleted = get_string("completedon", "quiz"); - $stroverdue = get_string("overdue", "quiz"); - /// Work out appropriate title. - if ($isteacher and $attempt->userid == $USER->id) { + if ($canpreview && $reviewofownattempt) { $strreviewtitle = get_string('reviewofpreview', 'quiz'); } else { $strreviewtitle = get_string('reviewofattempt', 'quiz', $attempt->attempt); } - $pagequestions = explode(',', $pagelist); +/// Print the page header $headtags = get_html_head_contributions($pagequestions, $questions, $states); - if (!empty($popup)) { - define('MESSAGE_WINDOW', true); // This prevents the message window coming up - print_header($course->shortname.': '.format_string($quiz->name), '', '', '', $headtags, false, '', '', false, ''); - /// Include Javascript protection for this page - include('protect_js.php'); + if ($accessmanager->securewindow_required($canpreview)) { + $accessmanager->setup_secure_page($course->shortname.': '.format_string($quiz->name), $headtags); } else { $strupdatemodule = has_capability('moodle/course:manageactivities', $coursecontext) ? update_module_button($cm->id, $course->id, get_string('modulename', 'quiz')) @@ -136,9 +108,9 @@ } echo ''; // for overlib -/// Print heading and tabs if this is part of a preview - if (has_capability('mod/quiz:preview', $context)) { - if ($attempt->userid == $USER->id) { // this is the report on a preview +/// Print tabs if they should be there. + if ($canpreview) { + if ($reviewofownattempt) { $currenttab = 'preview'; } else { $currenttab = 'reports'; @@ -146,27 +118,24 @@ } include('tabs.php'); } + +/// Print heading. print_heading(format_string($quiz->name)); - if ($isteacher and $attempt->userid == $USER->id) { - // the teacher is at the end of a preview. Print button to start new preview - unset($buttonoptions); - $buttonoptions['q'] = $quiz->id; - $buttonoptions['forcenew'] = true; - echo '
'; - print_single_button($CFG->wwwroot.'/mod/quiz/attempt.php', $buttonoptions, get_string('startagain', 'quiz')); - echo '
'; + if ($canpreview && $reviewofownattempt) { + print_restart_preview_button($quiz); } print_heading($strreviewtitle); - // print javascript button to close the window, if necessary - if (!$isteacher) { - include('attempt_close_js.php'); +/// Finish review link. + if ($reviewofownattempt) { + $accessmanager->print_finish_review_link($canpreview); } /// Print infobox - $timelimit = (int)$quiz->timelimit * 60; $overtime = 0; + $grade = quiz_rescale_grade($attempt->sumgrades, $quiz); + $feedback = quiz_feedback_for_grade($grade, $attempt->quiz); if ($attempt->timefinish) { if ($timetaken = ($attempt->timefinish - $attempt->timestart)) { @@ -205,13 +174,13 @@ echo '', get_string('startedon', 'quiz'), '', userdate($attempt->timestart), ''; if ($attempt->timefinish) { - echo '', $strtimecompleted, '', + echo '', get_string('completedon', 'quiz'), '', userdate($attempt->timefinish), ''; - echo '', $strtimetaken, '', + echo '', get_string('timetaken', 'quiz'), '', $timetaken, ''; } if (!empty($overtime)) { - echo '', $stroverdue, '',$overtime, ''; + echo '', get_string('overdue', 'quiz'), '',$overtime, ''; } //if the student is allowed to see their score if ($options->scores) { @@ -226,9 +195,9 @@ $a->grade = $grade; $a->maxgrade = $quiz->grade; $rawscore = round($attempt->sumgrades, $CFG->quiz_decimalpoints); - echo '', $strscore, '', + echo '', get_string('score', 'quiz'), '', "$rawscore/$quiz->sumgrades ($percentage%)", ''; - echo '', $strgrade, '', + echo '', get_string('grade'), '', get_string('outof', 'quiz', $a), ''; } } @@ -248,8 +217,7 @@ } /// Print all the questions - - $number = quiz_first_questionnumber($attempt->layout, $pagelist); + $number = quiz_first_questionnumber($attempt->layout, $questionlist); foreach ($pagequestions as $i) { if (!isset($questions[$i])) { print_simple_box_start('center', '90%'); @@ -260,7 +228,7 @@ continue; } $options->validation = QUESTION_EVENTVALIDATE === $states[$i]->event; - $options->history = ($isteacher and !$attempt->preview) ? 'all' : 'graded'; + $options->history = ($canpreview and !$attempt->preview) ? 'all' : 'graded'; // Print the question print_question($questions[$i], $states[$i], $number, $quiz, $options); $number += $questions[$i]->length; @@ -272,11 +240,13 @@ } // print javascript button to close the window, if necessary - if (!$isteacher) { - include('attempt_close_js.php'); + if ($reviewofownattempt) { + $accessmanager->print_finish_review_link($canpreview); } - if (empty($popup)) { + if ($accessmanager->securewindow_required($canpreview)) { + print_footer('empty'); + } else { print_footer($course); } ?> diff --git a/mod/quiz/simpletest/testaccessrules.php b/mod/quiz/simpletest/testaccessrules.php index 018a6f2186..efe3cf75c3 100644 --- a/mod/quiz/simpletest/testaccessrules.php +++ b/mod/quiz/simpletest/testaccessrules.php @@ -356,25 +356,25 @@ class password_access_rule_test extends UnitTestCase { $rule->clear_access_allowed(-1); $_POST['cancelpassword'] = false; $_POST['quizpassword'] = ''; - $html = $rule->do_password_check(true); + $html = $rule->do_password_check(false, null, true); $this->assertPattern($reqpwregex, $html); $this->assertPattern('/SOME INTRO TEXT/', $html); $this->assertNoPattern($pwerrregex, $html); $_POST['quizpassword'] = 'toad'; - $html = $rule->do_password_check(true); + $html = $rule->do_password_check(false, null, true); $this->assertPattern($reqpwregex, $html); $this->assertPattern($pwerrregex, $html); $_POST['quizpassword'] = 'frog'; - $this->assertNull($rule->do_password_check(true)); + $this->assertNull($rule->do_password_check(false, null, true)); // Check that once you are in, the password isn't checked again. $_POST['quizpassword'] = 'newt'; - $this->assertNull($rule->do_password_check(true)); + $this->assertNull($rule->do_password_check(false, null, true)); $rule->clear_access_allowed(-1); - $html = $rule->do_password_check(true); + $html = $rule->do_password_check(false, null, true); $this->assertPattern($reqpwregex, $html); } } diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 27e7b65407..13a33fb879 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -1,6 +1,6 @@ libdir.'/blocklib.php'); @@ -8,9 +8,8 @@ require_once($CFG->dirroot.'/mod/quiz/locallib.php'); require_once($CFG->dirroot.'/mod/quiz/pagelib.php'); - $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or - $q = optional_param('q', 0, PARAM_INT); // quiz ID - $edit = optional_param('edit', -1, PARAM_BOOL); + $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or + $q = optional_param('q', 0, PARAM_INT); // quiz ID if ($id) { if (! $cm = get_coursemodule_from_id('quiz', $id)) { @@ -34,125 +33,112 @@ } } - // Check login and get context. +/// Check login and get context. require_login($course->id, false, $cm); $context = get_context_instance(CONTEXT_MODULE, $cm->id); + require_capability('mod/quiz:view', $context); + +/// Cache some other capabilites we use several times. + $canattempt = has_capability('mod/quiz:attempt', $context); + $canpreview = has_capability('mod/quiz:preview', $context); - // if no questions have been set up yet redirect to edit.php - if (!$quiz->questions and has_capability('mod/quiz:manage', $context)) { +/// Create an object to manage all the other (non-roles) access rules. + $timenow = time(); + $accessmanager = new quiz_access_manager($quiz, $timenow, + has_capability('mod/quiz:ignoretimelimits', $context, NULL, false)); + +/// If no questions have been set up yet redirect to edit.php + if (!$quiz->questions && has_capability('mod/quiz:manage', $context)) { redirect('edit.php?cmid='.$cm->id); } +/// Log this request. add_to_log($course->id, "quiz", "view", "view.php?id=$cm->id", $quiz->id, $cm->id); - // Initialize $PAGE, compute blocks +/// Initialize $PAGE, compute blocks $PAGE = page_create_instance($quiz->id); $pageblocks = blocks_setup($PAGE); $blocks_preferred_width = bounded_number(180, blocks_preferred_width($pageblocks[BLOCK_POS_LEFT]), 210); - // Print the page header - if ($edit != -1 and $PAGE->user_allowed_editing()) { + $edit = optional_param('edit', -1, PARAM_BOOL); + if ($edit != -1 && $PAGE->user_allowed_editing()) { $USER->editing = $edit; } - //only check pop ups if the user is not a teacher, and popup is set - - $bodytags = (has_capability('mod/quiz:attempt', $context) && $quiz->popup)?'onload="popupchecker(\'' . get_string('popupblockerwarning', 'quiz') . '\');"':''; +/// Print the page header + $bodytags = ''; + if ($accessmanager->securewindow_required($canpreview)) { + $bodytags = 'onload="popupchecker(\'' . get_string('popupblockerwarning', 'quiz') . '\');"'; + } + require_js('yui_yahoo'); + require_js('yui_event'); $PAGE->print_header($course->shortname.': %fullname%','',$bodytags); +/// Print any blocks on the left of the page. echo ''; - if(!empty($CFG->showblocksonmodpages) && (blocks_have_content($pageblocks, BLOCK_POS_LEFT) || $PAGE->user_is_editing())) { echo ''; + echo "\n"; } +/// Start the main part of the page echo '
'; print_container_start(); blocks_print_group($PAGE, $pageblocks, BLOCK_POS_LEFT); print_container_end(); - echo ''; print_container_start(); - // Print the main part of the page - - // Print heading and tabs (if there is more than one). +/// Print heading and tabs (if there is more than one). $currenttab = 'info'; include('tabs.php'); - // Print quiz name - +/// Print quiz name and description print_heading(format_string($quiz->name)); + if (trim(strip_tags($quiz->intro))) { + $formatoptions->noclean = true; + print_box(format_text($quiz->intro, FORMAT_MOODLE, $formatoptions), 'generalbox', 'intro'); + } - if (has_capability('mod/quiz:view', $context)) { - - // Print quiz description - if (trim(strip_tags($quiz->intro))) { - $formatoptions->noclean = true; - print_box(format_text($quiz->intro, FORMAT_MOODLE, $formatoptions), 'generalbox', 'intro'); - } - - echo '
'; - - // Print information about number of attempts and grading method. - if ($quiz->attempts > 1) { - echo "

".get_string("attemptsallowed", "quiz").": $quiz->attempts

"; - } - if ($quiz->attempts != 1) { - echo "

".get_string("grademethod", "quiz").": ".quiz_get_grading_option_name($quiz->grademethod)."

"; - } - - // Print information about timings. - $timenow = time(); - $available = ($quiz->timeopen < $timenow and ($timenow < $quiz->timeclose or !$quiz->timeclose)); - if ($available) { - if ($quiz->timelimit) { - echo "

".get_string("quiztimelimit","quiz", format_time($quiz->timelimit * 60))."

"; - } - if ($quiz->timeopen) { - echo '

', get_string('quizopens', 'quiz'), ': ', userdate($quiz->timeopen), '

'; - } - if ($quiz->timeclose) { - echo '

', get_string('quizcloses', 'quiz'), ': ', userdate($quiz->timeclose), '

'; - } - } else if ($timenow < $quiz->timeopen) { - echo "

".get_string("quiznotavailable", "quiz", userdate($quiz->timeopen))."

"; - } else { - echo "

".get_string("quizclosed", "quiz", userdate($quiz->timeclose))."

"; - } - echo '
'; - } else { - $available = false; +/// Display information about this quiz. + $messages = $accessmanager->describe_rules(); + if ($quiz->attempts != 1) { + $messages[] = get_string('gradingmethod', 'quiz', quiz_get_grading_option_name($quiz->grademethod)); } + print_box_start('quizinfo'); + $accessmanager->print_messages($messages); + print_box_end(); - // Show number of attempts summary to those who can view reports. +/// Show number of attempts summary to those who can view reports. if (has_capability('mod/quiz:viewreports', $context)) { if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm)) { echo ''; + $cm->id . '">' . $strattemptnum . "\n"; } } - // Guests can't do a quiz, so offer them a choice of logging in or going back. +/// Guests can't do a quiz, so offer them a choice of logging in or going back. if (isguestuser()) { $loginurl = $CFG->wwwroot.'/login/index.php'; if (!empty($CFG->loginhttps)) { $loginurl = str_replace('http:','https:', $loginurl); } - notice_yesno('

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

\n\n

" . - get_string('liketologin') . '

', $loginurl, get_referer(false)); + notice_yesno('

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

\n\n

" . + get_string('liketologin') . "

\n", $loginurl, get_referer(false)); finish_page($course); } - if (!(has_capability('mod/quiz:attempt', $context) || has_capability('mod/quiz:preview', $context))) { - print_box('

' . get_string('youneedtoenrol', 'quiz') . '

' . +/// If they are not using guest access, and they can't do the quiz, tell them that. + if (!($canattempt || $canpreview)) { + print_box('

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

\n\n

" . print_continue($CFG->wwwroot . '/course/view.php?id=' . $course->id, true) . - '

', 'generalbox', 'notice'); + "

\n", 'generalbox', 'notice'); finish_page($course); } - // Get this user's attempts. +/// Get this user's attempts. $attempts = quiz_get_user_attempts($quiz->id, $USER->id); + $lastfinishedattempt = end($attempts); $unfinished = false; if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { $attempts[] = $unfinishedattempt; @@ -160,7 +146,7 @@ } $numattempts = count($attempts); - // Work out the final grade, checking whether it was overridden in the gradebook. +/// Work out the final grade, checking whether it was overridden in the gradebook. $mygrade = quiz_get_best_grade($quiz, $USER->id); $mygradeoverridden = false; $gradebookfeedback = ''; @@ -181,7 +167,7 @@ } } - // Print table with existing attempts +/// Print table with existing attempts if ($attempts) { print_heading(get_string('summaryofattempts', 'quiz')); @@ -204,7 +190,7 @@ $feedbackcolumn = quiz_has_feedback($quiz->id); $overallfeedback = $feedbackcolumn && $alloptions->overallfeedback; - // prepare table header + // Prepare table header $table->class = 'generaltable quizattemptsummary'; $table->head = array($strattempt, $strtimecompleted); $table->align = array("center", "left"); @@ -237,9 +223,9 @@ // Add the attempt number, making it a link, if appropriate. if ($attempt->preview) { - $row[] = make_review_link(get_string('preview', 'quiz'), $quiz, $attempt); + $row[] = $accessmanager->make_review_link(get_string('preview', 'quiz'), $attempt, $canpreview, $attemptoptions); } else { - $row[] = make_review_link($attempt->attempt, $quiz, $attempt); + $row[] = $accessmanager->make_review_link($attempt->attempt, $attempt, $canpreview, $attemptoptions); } // prepare strings for time taken and date completed @@ -249,24 +235,20 @@ // attempt has finished $timetaken = format_time($attempt->timefinish - $attempt->timestart); $datecompleted = userdate($attempt->timefinish); - } else if ($available) { + } else if (!$quiz->timeclose || $timenow < $quiz->timeclose) { // The attempt is still in progress. - $timetaken = format_time(time() - $attempt->timestart); + $timetaken = format_time($timenow - $attempt->timestart); $datecompleted = ''; - } else if ($quiz->timeclose) { - // The attempt was not completed but is also not available any more becuase the quiz is closed. + } else { $timetaken = format_time($quiz->timeclose - $attempt->timestart); $datecompleted = userdate($quiz->timeclose); - } else { - // Something weird happened. - $timetaken = ''; - $datecompleted = ''; } $row[] = $datecompleted; if ($markcolumn && $attempt->timefinish > 0) { if ($attemptoptions->scores) { - $row[] = make_review_link(round($attempt->sumgrades, $quiz->decimalpoints), $quiz, $attempt); + $row[] = $accessmanager->make_review_link(round($attempt->sumgrades, $quiz->decimalpoints), + $attempt, $canpreview, $attemptoptions); } else { $row[] = ''; } @@ -279,11 +261,12 @@ if ($attemptoptions->scores && $attempt->timefinish > 0) { $formattedgrade = $attemptgrade; // highlight the highest grade if appropriate - if ($overallstats && $numattempts > 1 && !is_null($mygrade) && $attemptgrade == $mygrade && $quiz->grademethod == QUIZ_GRADEHIGHEST) { + if ($overallstats && !$attempt->preview && $numattempts > 1 && !is_null($mygrade) && + $attemptgrade == $mygrade && $quiz->grademethod == QUIZ_GRADEHIGHEST) { $table->rowclass[$attempt->attempt] = 'bestrow'; } - $row[] = make_review_link($formattedgrade, $quiz, $attempt); + $row[] = $accessmanager->make_review_link($formattedgrade, $attempt, $canpreview, $attemptoptions); } else { $row[] = ''; } @@ -301,13 +284,17 @@ $row[] = $timetaken; } - $table->data[$attempt->attempt] = $row; + if ($attempt->preview) { + $table->data['preview'] = $row; + } else { + $table->data[$attempt->attempt] = $row; + } } // End of loop over attempts. print_table($table); } - // Print information about the student's best score for this quiz if possible. - $moreattempts = $unfinished || $numattempts < $quiz->attempts || $quiz->attempts == 0; +/// Print information about the student's best score for this quiz if possible. + $moreattempts = $unfinished || !$accessmanager->is_finished($numattempts, $lastfinishedattempt); if (!$moreattempts) { print_heading(get_string("nomoreattempts", "quiz")); } @@ -316,7 +303,7 @@ $resultinfo = ''; if ($overallstats) { - if ($available && $moreattempts) { + if ($moreattempts) { $a = new stdClass; $a->method = quiz_get_grading_option_name($quiz->grademethod); $a->mygrade = $mygrade; @@ -325,18 +312,18 @@ } else { $resultinfo .= print_heading(get_string('yourfinalgradeis', 'quiz', "$mygrade / $quiz->grade"), '', 2, 'main', true); if ($mygradeoverridden) { - $resultinfo .= '

'.get_string('overriddennotice', 'grades').'

'; + $resultinfo .= '

'.get_string('overriddennotice', 'grades')."

\n"; } } } if ($gradebookfeedback) { $resultinfo .= print_heading(get_string('comment', 'quiz'), '', 3, 'main', true); - $resultinfo .= '

'.$gradebookfeedback.'

'; + $resultinfo .= '

'.$gradebookfeedback."

\n"; } if ($overallfeedback) { $resultinfo .= print_heading(get_string('overallfeedback', 'quiz'), '', 3, 'main', true); - $resultinfo .= '

'.quiz_feedback_for_grade($mygrade, $quiz->id).'

'; + $resultinfo .= '

'.quiz_feedback_for_grade($mygrade, $quiz->id)."

\n"; } if ($resultinfo) { @@ -344,107 +331,52 @@ } } - // Print a button to start/continue an attempt, if appropriate. +/// Determine if we should be showing a start/continue attempt button, +/// or a button to go back to the course page. + print_box_start('quizattempt'); + $buttontext = ''; // This will be set something if as start/continue attempt button should appear. if (!$quiz->questions) { print_heading(get_string("noquestions", "quiz")); - - } else if ($available && $moreattempts) { - echo "
"; - echo "
"; - + } else { if ($unfinished) { - if (has_capability('mod/quiz:preview', $context)) { + if ($canpreview) { $buttontext = get_string('continuepreview', 'quiz'); } else { $buttontext = get_string('continueattemptquiz', 'quiz'); } } else { - - // Work out the appropriate button caption. - if (has_capability('mod/quiz:preview', $context)) { - $buttontext = get_string('previewquiznow', 'quiz'); - } else if ($numattempts == 0) { - $buttontext = get_string('attemptquiznow', 'quiz'); + $messages = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt); + if (!$canpreview && $messages) { + $accessmanager->print_messages($messages); } else { - $buttontext = get_string('reattemptquiz', 'quiz'); - } - - // Work out if the quiz is temporarily unavailable because of the delay option. - if (!empty($attempts)) { - $tempunavailable = ''; - $lastattempt = end($attempts); - $lastattempttime = $lastattempt->timefinish; - if ($numattempts == 1 && $quiz->delay1 && $timenow <= $lastattempttime + $quiz->delay1) { - $tempunavailable = get_string('temporaryblocked', 'quiz') . - ' '. userdate($lastattempttime + $quiz->delay1). ''; - } else if ($numattempts > 1 && $quiz->delay2 && $timenow <= $lastattempttime + $quiz->delay2) { - $tempunavailable = get_string('temporaryblocked', 'quiz') . - ' '. userdate($lastattempttime + $quiz->delay2). ''; - } - - // If so, display a message and prevent the start button from appearing. - if ($tempunavailable) { - print_simple_box($tempunavailable, "center"); - print_continue($CFG->wwwroot . '/course/view.php?id=' . $course->id); - $buttontext = ''; + if ($canpreview) { + $buttontext = get_string('previewquiznow', 'quiz'); + } else if ($numattempts == 0) { + $buttontext = get_string('attemptquiznow', 'quiz'); + } else { + $buttontext = get_string('reattemptquiz', 'quiz'); } } } - // Actually print the start button. + // If, so far, we think a button should be printed, so check if they will be allowed to access it. if ($buttontext) { - $buttontext = htmlspecialchars($buttontext, ENT_QUOTES); - - // Do we need a confirm javascript alert? - if ($unfinished) { - $strconfirmstartattempt = ''; - } else if ($quiz->timelimit && $quiz->attempts) { - $strconfirmstartattempt = get_string('confirmstartattempttimelimit','quiz', $quiz->attempts); - } else if ($quiz->timelimit) { - $strconfirmstartattempt = get_string('confirmstarttimelimit','quiz'); - } else if ($quiz->attempts) { - $strconfirmstartattempt = get_string('confirmstartattemptlimit','quiz', $quiz->attempts); - } else { - $strconfirmstartattempt = ''; + if (!$moreattempts) { + $buttontext = ''; + } else if (!$canpreview && $messages = $accessmanager->prevent_access()) { + $accessmanager->print_messages($messages); + $buttontext = ''; } - // Determine the URL to use. - $attempturl = "attempt.php?id=$cm->id"; - - // Prepare options depending on whether the quiz should be a popup. - if (!empty($quiz->popup)) { - $window = 'quizpopup'; - $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"; - if (!empty($CFG->usesid) && !isset($_COOKIE[session_name()])) { - $attempturl = sid_process_url($attempturl); - } - - echo ''; - } else { - print_single_button("attempt.php", array('id'=>$cm->id), $buttontext, 'get', '', false, '', false, $strconfirmstartattempt); - } - - -?> - -\n"; +/// Now actually print the appropriate button. + if ($buttontext) { + $accessmanager->print_start_attempt_button($canpreview, $buttontext, $unfinished); } else { print_continue($CFG->wwwroot . '/course/view.php?id=' . $course->id); } + print_box_end(); // Should we not be seeing if we need to print right-hand-side blocks? @@ -459,37 +391,4 @@ function finish_page($course) { print_footer($course); exit; } - -/** Make some text into a link to review the quiz, if that is appropriate. */ -function make_review_link($linktext, $quiz, $attempt) { - // If not even responses are to be shown in review then we don't allow any review - if (!($quiz->review & QUIZ_REVIEW_RESPONSES)) { - return $linktext; - } - - // If the quiz is still open, are reviews allowed? - if ((!$quiz->timeclose or time() < $quiz->timeclose) and !($quiz->review & QUIZ_REVIEW_OPEN)) { - // If not, don't link. - return $linktext; - } - - // If the quiz is closed, are reviews allowed? - if (($quiz->timeclose and time() > $quiz->timeclose) and !($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; - } - - $url = "review.php?q=$quiz->id&attempt=$attempt->id"; - if ($quiz->popup) { - $windowoptions = "left=0, top=0, channelmode=yes, fullscreen=yes, scrollbars=yes, resizeable=no, directories=no, toolbar=no, titlebar=no, location=no, status=no, menubar=no"; - return link_to_popup_window('/mod/quiz/' . $url, 'quizpopup', $linktext, '+window.screen.height+', '+window.screen.width+', '', $windowoptions, true); - } else { - return "$linktext"; - } -} ?> diff --git a/theme/standard/styles_layout.css b/theme/standard/styles_layout.css index d3eb49e1e5..28479819c7 100644 --- a/theme/standard/styles_layout.css +++ b/theme/standard/styles_layout.css @@ -3769,7 +3769,7 @@ body#mod-forum-search .introcontent { #mod-quiz-view .generalbox#feedback h2 { margin: 0 0; } -body#mod-quiz-view .generalbox#feedback .overriddennotice { +#mod-quiz-view .generalbox#feedback .overriddennotice { text-align: center; font-size: 0.7em; } @@ -3795,14 +3795,18 @@ body#mod-quiz-view .generalbox#feedback .overriddennotice { text-align: center; } -#mod-quiz-attempt #timer .generalbox { - width:150px +#mod-quiz-attempt #quiz-timer-outer { + position: absolute; + width: 150px; + top: 100px; + left: 10px; + padding: 0.25em 0; + border-width: 1px; + border-style: solid; } - -#mod-quiz-attempt #timer { - position:absolute; - /*top:100px; is set by js*/ - left:10px +#mod-quiz-attempt #quiz-timer-outer h3, +#mod-quiz-attempt #quiz-timer-outer p { + margin: 0; } body#question-preview .quemodname, @@ -3815,7 +3819,8 @@ body#question-preview .quemodname, body#question-preview .controls { } #mod-quiz-attempt #page .controls, -#mod-quiz-review #page .controls { +#mod-quiz-review #page .controls, +#mod-quiz-review #page .finishreview { text-align: center; margin: 8px auto; }