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.
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This class handles loading all the information about a quiz attempt into memory,
|
||||
* and making it available for attemtp.php, summary.php and review.php.
|
||||
* Initially, it only loads a minimal amout of information about each attempt - loading
|
||||
* extra information only when necessary or when asked. The class tracks which questions
|
||||
* are loaded.
|
||||
*/
|
||||
|
||||
require_once("../../config.php");
|
||||
|
||||
/**
|
||||
* Class for quiz exceptions.
|
||||
*
|
||||
*/
|
||||
class moodle_quiz_exception extends moodle_exception {
|
||||
function __construct($quizobj, $errorcode, $a = NULL, $link = '', $debuginfo = null) {
|
||||
if (!$link) {
|
||||
$link = $quizobj->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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php // $Id$
|
||||
/**
|
||||
* This page prints a summary of a quiz attempt before it is submitted.
|
||||
*
|
||||
* @author Tim Hunt others.
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
* @package quiz
|
||||
*/
|
||||
|
||||
require_once("../../config.php");
|
||||
require_once("locallib.php");
|
||||
require_once("attemptlib.php");
|
||||
|
||||
$attemptid = required_param('attempt', PARAM_INT); // The attempt to summarise.
|
||||
$attemptobj = new quiz_attempt($attemptid);
|
||||
|
||||
/// Check login and get contexts.
|
||||
require_login($attemptobj->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 "<div class=\"submitbtns mdl-align\">\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 "</div>\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);
|
||||
}
|
||||
|
||||
?>
|
||||
+4
-1
@@ -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 = '';
|
||||
|
||||
Reference in New Issue
Block a user