From: tjhunt Date: Fri, 27 Jun 2008 18:04:48 +0000 (+0000) Subject: MDL-15452 - Put the OU quiz navigation improvements into the Moodle codebase X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=36e413e38c5d04c923f62e2525b2d59767babfc8;p=moodle.git MDL-15452 - Put the OU quiz navigation improvements into the Moodle codebase Part one, a first cut of a summary page, along with some classes that will allow me to eliminate more duplicated code between the new page, attempt.php and review.php. --- diff --git a/lang/en_utf8/quiz.php b/lang/en_utf8/quiz.php index 0611cef468..97c2b84024 100644 --- a/lang/en_utf8/quiz.php +++ b/lang/en_utf8/quiz.php @@ -53,7 +53,7 @@ $string['attempt'] = 'Attempt $a'; $string['attemptclosed'] = 'Attempt has not closed yet'; $string['attemptduration'] = 'Time taken'; $string['attemptedon'] = 'Attempted on'; -$string['attempterror'] = 'Attempt error'; +$string['attempterror'] = 'You are not allowed to attempt this quiz at this time because: $a'; $string['attemptfirst'] = 'First attempt'; $string['attemptincomplete'] = 'That attempt (by $a) is not yet completed.'; $string['attemptlast'] = 'Last attempt'; @@ -307,6 +307,7 @@ $string['importquestions'] = 'Import questions from file'; $string['incorrect'] = 'Incorrect'; $string['indivresp'] = 'Responses of Individuals to Each Item'; $string['info'] = 'Info'; +$string['infoshort'] = 'i'; $string['introduction'] = 'Introduction'; $string['invalidattemptid'] = 'No such attempt ID exists'; $string['invalidcategory'] = 'Category ID is invalid'; @@ -435,6 +436,7 @@ $string['questionmissing'] = 'Question for this session is missing'; $string['questionname'] = 'Question name'; $string['questionnametoolong'] = 'Question name too long at line $a (255 char. max). It has been truncated.'; $string['questionno'] = 'Question $a'; +$string['questionnotloaded'] = 'Question $a has not been loaded from the database'; $string['questions'] = 'Questions'; $string['questionsinclhidden'] = 'Questions (including hidden)'; $string['questionsinuse'] = '(* Questions marked by an asterisk are already in use in some quizzes. These question will not be deleted from these quizzes but only from the category list.)'; @@ -572,11 +574,14 @@ $string['sortsubmit'] = 'Sort questions'; $string['sorttypealpha'] = 'Sort by type, name'; $string['startagain'] = 'Start again'; $string['startedon'] = 'Started on'; +$string['statenotloaded'] = 'The state for question $a has not been loaded from the database'; +$string['status'] = 'Status'; $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['summaryofattempt'] = 'Summary of attempt'; $string['summaryofattempts'] = 'Summary of your previous attempts'; $string['specificquestionnotonquiz'] = 'Specified question is not on the specified quiz'; $string['specificapathnotonquestion'] = 'The specified file path is not on the specified question'; diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php new file mode 100644 index 0000000000..7df4d43b7b --- /dev/null +++ b/mod/quiz/attemptlib.php @@ -0,0 +1,364 @@ +view_url(); + } + parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo); + } +} + +class quiz { + // Fields initialised in the constructor. + protected $course; + protected $cm; + protected $quiz; + protected $context; + + // Fields set later if that data is needed. + protected $accessmanager = null; + protected $reviewoptions = null; + protected $questions = array(); + protected $questionsnumbered = false; + + // Constructor ========================================================================= + function __construct($quiz, $cm, $course) { + $this->quiz = $quiz; + $this->cm = $cm; + $this->course = $course; + $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); + } + + // Functions for loading more data ===================================================== + public function load_questions_on_page($page) { + $this->load_questions(quiz_questions_on_page($this->quiz->layout, $page)); + } + + /** + * Load some or all of the queestions for this quiz. + * + * @param string $questionlist comma-separate list of question ids. Blank for all. + */ + public function load_questions($questionlist = '') { + if (!$questionlist) { + $questionlist = quiz_questions_in_quiz($this->quiz->layout); + } + $newquestions = question_load_questions($questionlist, 'qqi.grade AS maxgrade, qqi.id AS instance', + '{quiz_question_instances} qqi ON qqi.quiz = ' . $this->quiz->id . ' AND q.id = qqi.question'); + if (is_string($newquestions)) { + throw new moodle_quiz_exception($this, 'loadingquestionsfailed', $newquestions); + } + $this->questions = $this->questions + $newquestions; + $this->questionsnumbered = false; + } + + // Simple getters ====================================================================== + public function get_courseid() { + return $this->course->id; + } + + public function get_quizid() { + return $this->quiz->id; + } + + public function get_quiz() { + return $this->quiz; + } + + public function get_quiz_name() { + return $this->quiz->name; + } + + public function get_cmid() { + return $this->cm->id; + } + + public function get_cm() { + return $this->cm; + } + + public function get_question($id) { + $this->ensure_question_loaded($id); + return $this->questions[$id]; + } + + public function get_access_manager($timenow) { + if (is_null($this->accessmanager)) { + $this->accessmanager = new quiz_access_manager($this->quiz, $timenow, + has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false)); + } + return $this->accessmanager; + } + + // URLs related to this attempt ======================================================== + public function view_url() { + global $CFG; + return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id; + } + + // Bits of content ===================================================================== + public function update_module_button() { + if (has_capability('moodle/course:manageactivities', + get_context_instance(CONTEXT_COURSE, $this->course->id))) { + return update_module_button($this->cm->id, $this->course->id, get_string('modulename', 'quiz')); + } else { + return ''; + } + } + + public function navigation($title) { + return build_navigation($title, $this->cm); + } + + // Private methods ===================================================================== + private function ensure_question_loaded($id) { + if (!array_key_exists($id, $this->questions)) { + throw new moodle_quiz_exception($this, 'questionnotloaded', $id); + } + } +} + +class quiz_attempt extends quiz { + // Fields initialised in the constructor. + protected $attempt; + + // Fields set later if that data is needed. + protected $ispreview = null; + protected $states = array(); + + // Constructor ========================================================================= + function __construct($attemptid) { + global $DB; + if (!$this->attempt = quiz_load_attempt($attemptid)) { + throw new moodle_exception('invalidattemptid', 'quiz'); + } + if (!$quiz = $DB->get_record('quiz', array('id' => $this->attempt->quiz))) { + throw new moodle_exception('invalidquizid', 'quiz'); + } + if (!$course = $DB->get_record('course', array('id' => $quiz->course))) { + throw new moodle_exception('invalidcoursemodule'); + } + if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) { + throw new moodle_exception('invalidcoursemodule'); + } + parent::__construct($quiz, $cm, $course); + } + + // Functions for loading more data ===================================================== + public function load_questions_on_page($page) { + $this->load_questions(quiz_questions_on_page($this->attempt->layout, $page)); + } + + /** + * Load some or all of the queestions for this quiz. + * + * @param string $questionlist comma-separate list of question ids. Blank for all. + */ + public function load_questions($questionlist = '') { + if (!$questionlist) { + $questionlist = quiz_questions_in_quiz($this->attempt->layout); + } + parent::load_questions($questionlist); + } + + public function load_question_states() { + $questionstodo = array_diff_key($this->questions, $this->states); + if (!$newstates = get_question_states($questionstodo, $this->quiz, $this->attempt)) { + throw new moodle_quiz_exception($this, 'cannotrestore'); + } + $this->states = $this->states + $newstates; + } + + /** + * Number the loaded quetsions. + * + * At the moment, this assumes for simplicity that the loaded questions are contiguous. + */ + public function number_questions($page = 'all') { + if ($this->questionsnumbered) { + return; + } + if ($page != 'all') { + $pagelist = quiz_questions_in_page($this->attempt->layout, $page); + $number = quiz_first_questionnumber($this->attempt->layout, $pagelist); + } else { + $number = 1; + } + $questionids = $this->get_question_ids($page); + foreach ($questionids as $id) { + if ($this->questions[$id]->length > 0) { + $this->questions[$id]->number = $number; + $number += $this->questions[$id]->length; + } else { + $this->questions[$id]->number = get_string('infoshort', 'quiz'); + } + } + } + + // Simple getters ====================================================================== + public function get_attemptid() { + return $this->attempt->id; + } + + public function get_attempt() { + return $this->attempt; + } + + public function get_userid() { + return $this->attempt->userid; + } + + public function is_finished() { + return $this->attempt->timefinish != 0; + } + + public function is_preview() { + if (is_null($this->ispreview)) { + $this->ispreview = has_capability('mod/quiz:preview', $this->context); + } + return $this->ispreview; + } + + public function get_review_options() { + if (is_null($this->reviewoptions)) { + $this->reviewoptions = quiz_get_reviewoptions($this->quiz, $this->attempt, $this->context); + } + return $this->reviewoptions; + } + + public function get_question_ids($page = 'all') { + if ($page == 'all') { + $questionlist = quiz_questions_in_quiz($this->attempt->layout); + } else { + $questionlist = quiz_questions_in_page($this->attempt->layout, $page); + } + return explode(',', $questionlist); + } + + public function get_question_iterator($page = 'all') { + return new quiz_attempt_question_iterator($this, $page); + } + + public function get_question_status($questionid) { + //TODO + return 'FROG'; + } + + /** + * Return the grade obtained on a particular question, if the user ispermitted to see it. + * + * @param integer $questionid + * @return string the formatted grade, to the number of decimal places specified by the quiz. + */ + public function get_question_score($questionid) { + $this->ensure_state_loaded($questionid); + $options = quiz_get_renderoptions($this->quiz->review, $this->states[$questionid]); + if ($options->scores) { + return round($this->states[$questionid]->last_graded->grade, $this->quiz->decimalpoints); + } else { + return ''; + } + } + + // URLs related to this attempt ======================================================== + public function attempt_url($page = 0, $question = false) { + global $CFG; + $fragment = ''; + if ($question) { + $fragment = '#q' . $question; + } + return $CFG->wwwroot . '/mod/quiz/attempt.php?id=' . + $this->cm->id . '$amp;page=' . $page . $fragment; + } + + public function summary_url() { + global $CFG; + return $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $this->attempt->id; + } + + public function review_url($page = 0, $question = false, $showall = false) { + global $CFG; + $fragment = ''; + if ($question) { + $fragment = '#q' . $question; + } + $param = ''; + if ($showall) { + $param = '$amp;showall=1'; + } else if ($page) { + $param = '$amp;page=' . $page; + } + return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . + $this->attempt->id . $param . $fragment; + } + + + // Private methods ===================================================================== + private function ensure_state_loaded($id) { + if (!array_key_exists($id, $this->states)) { + throw new moodle_quiz_exception($this, 'statenotloaded', $id); + } + } +} + +class quiz_attempt_question_iterator implements Iterator { + private $attemptobj; + private $questionids; + public function __construct(quiz_attempt $attemptobj, $page = 'all') { + $this->attemptobj = $attemptobj; + $attemptobj->number_questions($page); + $this->questionids = $attemptobj->get_question_ids($page); + } + + public function rewind() { + reset($this->questionids); + } + + public function current() { + $id = current($this->questionids); + if ($id) { + return $this->attemptobj->get_question($id); + } else { + return false; + } + } + + public function key() { + $id = current($this->questionids); + if ($id) { + return $this->attemptobj->get_question($id)->number; + } else { + return false; + } + return $this->attemptobj->get_question(current($this->questionids))->number; + } + + public function next() { + $id = next($this->questionids); + if ($id) { + return $this->attemptobj->get_question($id); + } else { + return false; + } + } + + public function valid() { + return $this->current() !== false; + } +} +?> \ No newline at end of file diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php new file mode 100644 index 0000000000..bbb45e548c --- /dev/null +++ b/mod/quiz/summary.php @@ -0,0 +1,116 @@ +get_courseid(), false, $attemptobj->get_cm()); + +/// If this is not our own attempt, display an error. +if ($attemptobj->get_userid() != $USER->id) { + print_error('notyourattempt', 'quiz', $attemptobj->view_url()); +} + +/// If the attempt is already closed, redirect them to the review page. +if ($attemptobj->is_finished()) { + redirect($attemptobj->review_url()); +} + +/// Check access. +$accessmanager = $attemptobj->get_access_manager(time()); +$messages = $accessmanager->prevent_access(); +if (!$attemptobj->is_preview() && $messages) { + print_error('attempterror', 'quiz', $attemptobj->view_url(), + $accessmanager->print_messages($messages, true)); +} +$accessmanager->do_password_check($attemptobj->is_preview()); + +/// Log this page view. +add_to_log($attemptobj->get_courseid(), 'quiz', 'view summary', 'summary.php?attempt=' . $attemptobj->get_attemptid(), + $attemptobj->get_quizid(), $attemptobj->get_cmid()); + +/// Load the questions and states. +$attemptobj->load_questions(); +$attemptobj->load_question_states(); + +/// Print the page header +require_js($CFG->wwwroot . '/mod/quiz/quiz.js'); +$title = get_string('summaryofattempt', 'quiz'); +if ($accessmanager->securewindow_required($attemptobj->is_preview())) { + $accessmanager->setup_secure_page($course->shortname.': '.format_string($quiz->name), $headtags); +} else { + print_header_simple(format_string($attemptobj->get_quiz_name()), '', + $attemptobj->navigation($title), '', '', true, $attemptobj->update_module_button()); +} + +/// Print tabs if they should be there. +if ($attemptobj->is_preview()) { + $currenttab = 'preview'; + include('tabs.php'); +} + +/// Print heading. +print_heading(format_string($attemptobj->get_quiz_name())); +if ($attemptobj->is_preview()) { + print_restart_preview_button($quiz); +} +print_heading($title); + +/// Prepare the summary table header +$table->class = 'generaltable quizsummaryofattempt'; +$table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz')); +$table->align = array('left', 'left'); +$table->size = array('', ''); +$scorescolumn = $attemptobj->get_review_options()->scores; +if ($scorescolumn) { + $table->head[] = get_string('marks', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; +} +$table->data = array(); + +/// Get the summary info for each question. +$questionids = $attemptobj->get_question_ids(); +foreach ($attemptobj->get_question_iterator() as $number => $question) { + $row = array($number, $attemptobj->get_question_status($question->id)); + if ($scorescolumn) { + $row[] = $attemptobj->get_question_score($question->id); + } + $table->data[] = $row; +} + +/// Print the summary table. +print_table($table); + +/// Finish attempt button. +echo "
\n"; +$options = array( + 'finishattempt' => 1, + 'timeup' => 0, + 'questionids' => '', + 'sesskey' => sesskey() +); +print_single_button($attemptobj->attempt_url(), $options, get_string('finishattempt', 'quiz'), + 'post', '', false, '', false, get_string('confirmclose', 'quiz')); +echo "
\n"; + +/// Finish the page +$accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time()); +if ($accessmanager->securewindow_required($attemptobj->is_preview())) { + print_footer('empty'); +} else { + print_footer($course); +} + +?> diff --git a/mod/quiz/tabs.php b/mod/quiz/tabs.php index f7e1306220..3b6a023131 100644 --- a/mod/quiz/tabs.php +++ b/mod/quiz/tabs.php @@ -8,7 +8,10 @@ */ global $DB; if (empty($quiz)) { - print_error('cannotcallscript'); + if (empty($attemptobj)) { + print_error('cannotcallscript'); + } + $quiz = $attemptobj->get_quiz(); } if (!isset($currenttab)) { $currenttab = '';