From a1eb3a4466cdae42141f408374cc65d6f1c0d6fd Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Tue, 8 Feb 2011 14:19:23 +0000 Subject: [PATCH] MDL-20636 It is now possible to start a quiz attempt. This includes merging the CSS. --- mod/quiz/attempt.php | 3 +- mod/quiz/attemptlib.php | 942 ++++++++++++++++++------------------ mod/quiz/lang/en/quiz.php | 5 +- mod/quiz/pix/navflagged.png | Bin 0 -> 101 bytes mod/quiz/startattempt.php | 9 +- mod/quiz/styles.css | 244 +++++----- question/engine/lib.php | 6 +- question/todo/diffstat.txt | 30 +- 8 files changed, 638 insertions(+), 601 deletions(-) create mode 100644 mod/quiz/pix/navflagged.png diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index ebd87ea60e6..bb111884943 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -86,7 +86,7 @@ add_to_log($attemptobj->get_courseid(), 'quiz', 'continue attempt', $attemptobj->get_quizid(), $attemptobj->get_cmid()); // Get the list of questions needed by this page. -$slots = $attemptobj->get_question_numbers($page); +$slots = $attemptobj->get_slots($page); // Check. if (empty($slots)) { @@ -139,7 +139,6 @@ if ($attemptobj->is_preview_user()) { echo '
', "\n"; echo '
'; -print_js_call('quiz_init_form'); // Print all the questions foreach ($slots as $slot) { diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 9039f2578b6..4a5fb5497d2 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -21,12 +21,21 @@ * There are classes for loading all the information about a quiz and attempts, * and for displaying the navigation panel. * - * @package quiz + * @package mod + * @subpackage quiz * @copyright 2008 onwards Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page. +} + + +// TODO get_question_score -> mark + + /** * Class for quiz exceptions. Just saves a couple of arguments on the * constructor for a moodle_exception. @@ -62,8 +71,7 @@ class quiz { protected $cm; protected $quiz; protected $context; - protected $questionids; // All question ids in order that they appear in the quiz. - protected $pagequestionids; // array page no => array of questionids on the page in order. + protected $questionids; // Fields set later if that data is needed. protected $questions = null; @@ -87,7 +95,7 @@ class quiz { if ($getcontext && !empty($cm->id)) { $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); } - $this->determine_layout(); + $this->questionids = explode(',', quiz_questions_in_quiz($this->quiz->questions)); } /** @@ -117,15 +125,6 @@ class quiz { } // Functions for loading more data ===================================================== - /** - * Convenience method. Calls {@link load_questions()} with the list of - * question ids for a given page. - * - * @param integer $page a page number. - */ - public function load_questions_on_page($page) { - $this->load_questions($this->pagequestionids[$page]); - } /** * Load just basic information about all the questions in this quiz. @@ -135,10 +134,9 @@ class quiz { throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url()); } $this->questions = question_preload_questions($this->questionids, - 'qqi.grade AS maxgrade, qqi.id AS instance', + 'qqi.grade AS maxmark, qqi.id AS instance', '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question', array('quizid' => $this->quiz->id)); - $this->number_questions(); } /** @@ -152,7 +150,9 @@ class quiz { } $questionstoprocess = array(); foreach ($questionids as $id) { - $questionstoprocess[$id] = $this->questions[$id]; + if (array_key_exists($id, $this->questions)) { + $questionstoprocess[$id] = $this->questions[$id]; + } } if (!get_question_options($questionstoprocess)) { throw new moodle_quiz_exception($this, 'loadingquestionsfailed', implode(', ', $questionids)); @@ -200,6 +200,11 @@ class quiz { return $this->cm; } + /** @return object the module context for this quiz. */ + public function get_context() { + return $this->context; + } + /** * @return boolean wether the current user is someone who previews the quiz, * rather than attempting it. @@ -212,19 +217,10 @@ class quiz { } /** - * @return integer number fo pages in this quiz. + * @return whether any questions have been added to this quiz. */ - public function get_num_pages() { - return count($this->pagequestionids); - } - - - /** - * @param int $page page number - * @return boolean true if this is the last page of the quiz. - */ - public function is_last_page($page) { - return $page == count($this->pagequestionids) - 1; + public function has_questions() { + return !empty($this->questionids); } /** @@ -244,33 +240,15 @@ class quiz { } $questions = array(); foreach ($questionids as $id) { + if (!array_key_exists($id, $this->questions)) { + throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url()); + } $questions[$id] = $this->questions[$id]; $this->ensure_question_loaded($id); } return $questions; } - /** - * Return the list of question ids for either a given page of the quiz, or for the - * whole quiz. - * - * @param mixed $page string 'all' or integer page number. - * @return array the reqested list of question ids. - */ - public function get_question_ids($page = 'all') { - if ($page === 'all') { - $list = $this->questionids; - } else { - $list = $this->pagequestionids[$page]; - } - // Clone the array, so our private arrays cannot be modified. - $result = array(); - foreach ($list as $id) { - $result[] = $id; - } - return $result; - } - /** * @param integer $timenow the current time as a unix timestamp. * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time. @@ -283,10 +261,6 @@ class quiz { return $this->accessmanager; } - public function get_overall_feedback($grade) { - return quiz_feedback_for_grade($grade, $this->quiz, $this->context, $this->cm); - } - /** * Wrapper round the has_capability funciton that automatically passes in the quiz context. */ @@ -366,67 +340,6 @@ class quiz { throw new moodle_quiz_exception($this, 'questionnotloaded', $id); } } - - /** - * Populate {@link $questionids} and {@link $pagequestionids} from the layout. - */ - protected function determine_layout() { - $this->questionids = array(); - $this->pagequestionids = array(); - - // Get the appropriate layout string (from quiz or attempt). - $layout = quiz_clean_layout($this->get_layout_string(), true); - if (empty($layout)) { - // Nothing to do. - return; - } - - // Break up the layout string into pages. - $pagelayouts = explode(',0', $layout); - - // Strip off any empty last page (normally there is one). - if (end($pagelayouts) == '') { - array_pop($pagelayouts); - } - - // File the ids into the arrays. - $this->questionids = array(); - $this->pagequestionids = array(); - foreach ($pagelayouts as $page => $pagelayout) { - $pagelayout = trim($pagelayout, ','); - if ($pagelayout == '') continue; - $this->pagequestionids[$page] = explode(',', $pagelayout); - foreach ($this->pagequestionids[$page] as $id) { - $this->questionids[] = $id; - } - } - } - - /** - * Number the questions, adding a _number field to each one. - */ - private function number_questions() { - $number = 1; - foreach ($this->pagequestionids as $page => $questionids) { - 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'); - } - $this->questions[$id]->_page = $page; - } - } - } - - /** - * @return string the layout of this quiz. Used by number_questions to - * work out which questions are on which pages. - */ - protected function get_layout_string() { - return $this->quiz->questions; - } } /** @@ -437,12 +350,14 @@ class quiz { * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ -class quiz_attempt extends quiz { +class quiz_attempt { // Fields initialised in the constructor. + protected $quizobj; protected $attempt; + protected $quba; // Fields set later if that data is needed. - protected $states = array(); + protected $pagelayout; // array page no => array of numbers on the page in order. protected $reviewoptions = null; // Constructor ========================================================================= @@ -456,9 +371,10 @@ class quiz_attempt extends quiz { */ function __construct($attempt, $quiz, $cm, $course) { $this->attempt = $attempt; - parent::__construct($quiz, $cm, $course); - $this->preload_questions(); - $this->preload_question_states(); + $this->quizobj = new quiz($quiz, $cm, $course); + $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid); + $this->determine_layout(); + $this->number_questions(); } /** @@ -482,68 +398,117 @@ class quiz_attempt extends quiz { if (!$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) { throw new moodle_exception('invalidcoursemodule'); } + // Update quiz with override information $quiz = quiz_update_effective_access($quiz, $attempt->userid); return new quiz_attempt($attempt, $quiz, $cm, $course); } - // Functions for loading more data ===================================================== - /** - * Load the state of a number of questions that have already been loaded. - * - * @param array $questionids question ids to process. Blank = all. - */ - public function load_question_states($questionids = null) { - if (is_null($questionids)) { - $questionids = $this->questionids; + private function determine_layout() { + $this->pagelayout = array(); + + // Break up the layout string into pages. + $pagelayouts = explode(',0', quiz_clean_layout($this->attempt->layout, true)); + + // Strip off any empty last page (normally there is one). + if (end($pagelayouts) == '') { + array_pop($pagelayouts); } - $questionstoprocess = array(); - foreach ($questionids as $id) { - $this->ensure_question_loaded($id); - $questionstoprocess[$id] = $this->questions[$id]; + + // File the ids into the arrays. + $this->pagelayout = array(); + foreach ($pagelayouts as $page => $pagelayout) { + $pagelayout = trim($pagelayout, ','); + if ($pagelayout == '') { + continue; + } + $this->pagelayout[$page] = explode(',', $pagelayout); } - if (!question_load_states($questionstoprocess, $this->states, - $this->quiz, $this->attempt)) { - throw new moodle_quiz_exception($this, 'cannotrestore'); + } + + // Number the questions. + private function number_questions() { + $number = 1; + foreach ($this->pagelayout as $page => $slots) { + foreach ($slots as $slot) { + $question = $this->quba->get_question($slot); + if ($question->length > 0) { + $question->_number = $number; + $number += $question->length; + } else { + $question->_number = get_string('infoshort', 'quiz'); + } + $question->_page = $page; + } } } + // Simple getters ====================================================================== + public function get_quiz() { + return $this->quizobj->get_quiz(); + } + + public function get_quizobj() { + return $this->quizobj; + } + + /** @return integer the course id. */ + public function get_courseid() { + return $this->quizobj->get_courseid(); + } + + /** @return integer the course id. */ + public function get_course() { + return $this->quizobj->get_course(); + } + + /** @return integer the quiz id. */ + public function get_quizid() { + return $this->quizobj->get_quizid(); + } + + /** @return string the name of this quiz. */ + public function get_quiz_name() { + return $this->quizobj->get_quiz_name(); + } + + /** @return object the course_module object. */ + public function get_cm() { + return $this->quizobj->get_cm(); + } + + /** @return object the course_module object. */ + public function get_cmid() { + return $this->quizobj->get_cmid(); + } + /** - * Load basic information about the state of each question. - * - * This is enough to, for example, show the state of each question in the - * navigation panel, but only takes one DB query. + * @return boolean wether the current user is someone who previews the quiz, + * rather than attempting it. */ - public function preload_question_states() { - if (empty($this->questionids)) { - throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url()); - } - $this->states = question_preload_states($this->attempt->uniqueid); - if (!$this->states) { - $this->states = array(); - } + public function is_preview_user() { + return $this->quizobj->is_preview_user(); + } + + /** @return integer the number of attempts allowed at this quiz (0 = infinite). */ + public function get_num_attempts_allowed() { + return $this->quizobj->get_num_attempts_allowed(); + } + + /** @return integer number fo pages in this quiz. */ + public function get_num_pages() { + return count($this->pagelayout); } /** - * Load a particular state of a particular question. Used by the reviewquestion.php - * script to let the teacher walk through the entire sequence of a student's - * interaction with a question. - * - * @param $questionid the question id - * @param $stateid the id of the particular state to load. + * @param integer $timenow the current time as a unix timestamp. + * @return quiz_access_manager and instance of the quiz_access_manager class for this quiz at this time. */ - public function load_specific_question_state($questionid, $stateid) { - global $DB; - $state = question_load_specific_state($this->questions[$questionid], - $this->quiz, $this->attempt->uniqueid, $stateid); - if ($state === false) { - throw new moodle_quiz_exception($this, 'invalidstateid'); - } - $this->states[$questionid] = $state; + public function get_access_manager($timenow) { + return $this->quizobj->get_access_manager($timenow); } - // Simple getters ====================================================================== /** @return integer the attempt id. */ public function get_attemptid() { return $this->attempt->id; @@ -592,13 +557,27 @@ class quiz_attempt extends quiz { (!$this->is_preview_user() || $this->attempt->preview); } + /** + * Wrapper round the has_capability funciton that automatically passes in the quiz context. + */ + public function has_capability($capability, $userid = NULL, $doanything = true) { + return $this->quizobj->has_capability($capability, $userid, $doanything); + } + + /** + * Wrapper round the require_capability funciton that automatically passes in the quiz context. + */ + public function require_capability($capability, $userid = NULL, $doanything = true) { + return $this->quizobj->require_capability($capability, $userid, $doanything); + } + /** * Check the appropriate capability to see whether this user may review their own attempt. * If not, prints an error. */ public function check_review_capability() { if (!$this->has_capability('mod/quiz:viewreports')) { - if ($this->get_review_options()->quizstate == QUIZ_STATE_IMMEDIATELY) { + if ($this->get_attempt_state() == mod_quiz_display_options::IMMEDIATELY_AFTER) { $this->require_capability('mod/quiz:attempt'); } else { $this->require_capability('mod/quiz:reviewmyattempts'); @@ -607,175 +586,228 @@ class quiz_attempt extends quiz { } /** - * Get the current state of a question in the attempt. - * - * @param $questionid a questionid. - * @return object the state. + * @return integer one of the mod_quiz_display_options::DURING, + * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants. */ - public function get_question_state($questionid) { - return $this->states[$questionid]; + public function get_attempt_state() { + return quiz_attempt_state($this->get_quiz(), $this->attempt); } /** - * Wrapper that calls quiz_get_reviewoptions with the appropriate arguments. + * Wrapper that the correct mod_quiz_display_options for this quiz at the + * moment. * - * @return object the review options for this user on this attempt. + * @return question_display_options the render options for this user on this attempt. */ - public function get_review_options() { - if (is_null($this->reviewoptions)) { - $this->reviewoptions = quiz_get_reviewoptions($this->quiz, $this->attempt, $this->context); + public function get_display_options($reviewing) { + if ($reviewing) { + if (is_null($this->reviewoptions)) { + $this->reviewoptions = quiz_get_reviewoptions($this->get_quiz(), + $this->attempt, $this->quizobj->get_context()); + } + return $this->reviewoptions; + + } else { + $options = mod_quiz_display_options::make_from_quiz($this->get_quiz(), + mod_quiz_display_options::DURING); + $options->flags = quiz_get_flag_option($this->attempt, $this->quizobj->get_context()); + return $options; } - return $this->reviewoptions; } /** - * Wrapper that calls get_render_options with the appropriate arguments. - * - * @param integer questionid the quetsion to get the render options for. - * @return object the render options for this user on this attempt. + * @param int $page page number + * @return boolean true if this is the last page of the quiz. */ - public function get_render_options($questionid) { - return quiz_get_renderoptions($this->quiz, $this->attempt, $this->context, - $this->get_question_state($questionid)); + public function is_last_page($page) { + return $page == count($this->pagelayout) - 1; } /** - * Get a quiz_attempt_question_iterator for either a page of the quiz, or a whole quiz. - * You must have called load_questions with an appropriate argument first. + * Return the list of question ids for either a given page of the quiz, or for the + * whole quiz. * - * @param mixed $page as for the @see{get_question_ids} method. - * @return quiz_attempt_question_iterator the requested iterator. + * @param mixed $page string 'all' or integer page number. + * @return array the reqested list of question ids. */ - public function get_question_iterator($page = 'all') { - return new quiz_attempt_question_iterator($this, $page); + public function get_slots($page = 'all') { + // TODO rename to get_slots + if ($page === 'all') { + $numbers = array(); + foreach ($this->pagelayout as $numbersonpage) { + $numbers = array_merge($numbers, $numbersonpage); + } + return $numbers; + } else { + return $this->pagelayout[$page]; + } } /** - * Return a summary of the current state of a question in this attempt. You must previously - * have called load_question_states to load the state data about this question. - * - * @param integer $questionid question id of a question that belongs to this quiz. - * @return string a brief string (that could be used as a CSS class name, for example) - * that describes the current state of a question in this attempt. Possible results are: - * open|saved|closed|correct|partiallycorrect|incorrect. + * Get the question_attempt object for a particular question in this attempt. + * @param integer $slot the number used to identify this question within this attempt. + * @return question_attempt */ - public function get_question_status($questionid) { - $state = $this->states[$questionid]; - switch ($state->event) { - case QUESTION_EVENTOPEN: - return 'open'; - - case QUESTION_EVENTSAVE: - case QUESTION_EVENTGRADE: - case QUESTION_EVENTSUBMIT: - return 'answered'; - - case QUESTION_EVENTCLOSEANDGRADE: - case QUESTION_EVENTCLOSE: - case QUESTION_EVENTMANUALGRADE: - $options = $this->get_render_options($questionid); - if ($options->scores && $this->questions[$questionid]->maxgrade > 0) { - return question_get_feedback_class($state->last_graded->raw_grade / - $this->questions[$questionid]->maxgrade); - } else { - return 'closed'; - } + public function get_question_attempt($slot) { + return $this->quba->get_question_attempt($slot); + } - default: - $a = new stdClass; - $a->event = $state->event; - $a->questionid = $questionid; - $a->attemptid = $this->attempt->id; - throw new moodle_quiz_exception($this, 'errorunexpectedevent', $a); - } + /** + * Is a particular question in this attempt a real question, or something like a description. + * @param integer $slot the number used to identify this question within this attempt. + * @return boolean whether that question is a real question. + */ + public function is_real_question($slot) { + return $this->quba->get_question($slot)->length != 0; } /** - * @param integer $questionid question id of a question that belongs to this quiz. - * @return boolean whether this question hss been flagged by the attempter. + * Is a particular question in this attempt a real question, or something like a description. + * @param integer $slot the number used to identify this question within this attempt. + * @return boolean whether that question is a real question. */ - public function is_question_flagged($questionid) { - $state = $this->states[$questionid]; - return $state->flagged; + public function is_question_flagged($slot) { + return $this->quba->get_question_attempt($slot)->is_flagged(); } /** * Return the grade obtained on a particular question, if the user is permitted to see it. * You must previously have called load_question_states to load the state data about this question. * - * @param integer $questionid question id of a question that belongs to this quiz. + * @param integer $slot the number used to identify this question within this attempt. * @return string the formatted grade, to the number of decimal places specified by the quiz. */ - public function get_question_score($questionid) { - $options = $this->get_render_options($questionid); - if ($options->scores) { - return quiz_format_question_grade($this->quiz, $this->states[$questionid]->last_graded->grade); - } else { - return ''; - } + public function get_question_number($slot) { + return $this->quba->get_question($slot)->_number; + } + + /** + * Return the grade obtained on a particular question, if the user is permitted to see it. + * You must previously have called load_question_states to load the state data about this question. + * + * @param integer $slot the number used to identify this question within this attempt. + * @return string the formatted grade, to the number of decimal places specified by the quiz. + */ + public function get_question_name($slot) { + return $this->quba->get_question($slot)->name; + } + + /** + * Return the grade obtained on a particular question, if the user is permitted to see it. + * You must previously have called load_question_states to load the state data about this question. + * + * @param integer $slot the number used to identify this question within this attempt. + * @param boolean $showcorrectness Whether right/partial/wrong states should + * be distinguised. + * @return string the formatted grade, to the number of decimal places specified by the quiz. + */ + public function get_question_status($slot, $showcorrectness) { + return $this->quba->get_question_state_string($slot, $showcorrectness); + } + + /** + * Return the grade obtained on a particular question. + * You must previously have called load_question_states to load the state + * data about this question. + * + * @param integer $slot the number used to identify this question within this attempt. + * @return string the formatted grade, to the number of decimal places specified by the quiz. + */ + public function get_question_score($slot) { + return quiz_format_question_grade($this->get_quiz(), $this->quba->get_question_mark($slot)); + } + + /** + * Get the time of the most recent action performed on a question. + * @param integer $slot the number used to identify this question within this usage. + * @return integer timestamp. + */ + public function get_question_action_time($slot) { + return $this->quba->get_question_action_time($slot); } // URLs related to this attempt ======================================================== /** + * @return string quiz view url. + */ + public function view_url() { + return $this->quizobj->view_url(); + } + + /** + * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter. + */ + public function start_attempt_url() { + return $this->quizobj->start_attempt_url(); + } + + /** + * @param integer $slot if speified, the slot number of a specific question to link to. + * @param integer $page if specified, a particular page to link to. If not givem deduced + * from $slot, or goes to the first page. * @param integer $questionid a question id. If set, will add a fragment to the URL * to jump to a particuar question on the page. - * @param integer $page if specified, the URL of this particular page of the attempt, otherwise - * the URL will go to the first page. If -1, deduce $page from $questionid. * @param integer $thispage if not -1, the current page. Will cause links to other things on * this page to be output as only a fragment. * @return string the URL to continue this attempt. */ - public function attempt_url($questionid = 0, $page = -1, $thispage = -1) { - return $this->page_and_question_url('attempt', $questionid, $page, false, $thispage); + public function attempt_url($slot = 0, $page = -1, $thispage = -1) { + return $this->page_and_question_url('attempt', $slot, $page, false, $thispage); } /** * @return string the URL of this quiz's summary page. */ public function summary_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $this->attempt->id; + return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id)); } /** * @return string the URL of this quiz's summary page. */ public function processattempt_url() { - global $CFG; - return $CFG->wwwroot . '/mod/quiz/processattempt.php'; + return new moodle_url('/mod/quiz/processattempt.php'); } /** - * @param integer $questionid a question id. If set, will add a fragment to the URL - * to jump to a particuar question on the page. If -1, deduce $page from $questionid. + * @param integer $slot indicates which question to link to. * @param integer $page if specified, the URL of this particular page of the attempt, otherwise - * the URL will go to the first page. + * the URL will go to the first page. If -1, deduce $page from $slot. * @param boolean $showall if true, the URL will be to review the entire attempt on one page, * and $page will be ignored. * @param integer $thispage if not -1, the current page. Will cause links to other things on * this page to be output as only a fragment. * @return string the URL to review this attempt. */ - public function review_url($questionid = 0, $page = -1, $showall = false, $thispage = -1) { - return $this->page_and_question_url('review', $questionid, $page, $showall, $thispage); + public function review_url($slot = 0, $page = -1, $showall = false, $thispage = -1) { + return $this->page_and_question_url('review', $slot, $page, $showall, $thispage); } // Bits of content ===================================================================== + /** * Initialise the JS etc. required all the questions on a page.. * @param mixed $page a page number, or 'all'. */ - public function get_html_head_contributions($page = 'all') { - global $PAGE; - question_get_html_head_contributions($this->get_question_ids($page), $this->questions, $this->states); + public function get_html_head_contributions($page = 'all', $showall = false) { + if ($showall) { + $page = 'all'; + } + $result = ''; + foreach ($this->get_slots($page) as $slot) { + $result .= $this->quba->render_question_head_html($slot); + } + $result .= question_engine::initialise_js(); + return $result; } /** * Initialise the JS etc. required by one question. * @param integer $questionid the question id. */ - public function get_question_html_head_contributions($questionid) { - question_get_html_head_contributions(array($questionid), $this->questions, $this->states); + public function get_question_html_head_contributions($slot) { + return $this->quba->render_question_head_html($slot) . + question_engine::initialise_js(); } /** @@ -799,52 +831,71 @@ class quiz_attempt extends quiz { } /** - * Wrapper round print_question from lib/questionlib.php. + * Generate the HTML that displayes the question in its current state, with + * the appropriate display options. * * @param integer $id the id of a question in this quiz attempt. * @param boolean $reviewing is the being printed on an attempt or a review page. * @param string $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. */ - public function print_question($id, $reviewing, $thispageurl = '') { - global $CFG; + public function render_question($slot, $reviewing, $thispageurl = '') { + return $this->quba->render_question($slot, + $this->get_display_options($reviewing), + $this->quba->get_question($slot)->_number); + } - if ($reviewing) { - $options = $this->get_review_options(); - } else { - $options = $this->get_render_options($id); - } - if ($thispageurl instanceof moodle_url) { - $thispageurl = $thispageurl->out(false); - } - if ($thispageurl) { - $this->quiz->thispageurl = str_replace($CFG->wwwroot, '', $thispageurl); - } else { - unset($thispageurl); - } - print_question($this->questions[$id], $this->states[$id], $this->questions[$id]->_number, - $this->quiz, $options); + /** + * Like {@link render_question()} but displays the question at the past step + * indicated by $seq, rather than showing the latest step. + * + * @param integer $id the id of a question in this quiz attempt. + * @param integer $seq the seq number of the past state to display. + * @param boolean $reviewing is the being printed on an attempt or a review page. + * @param string $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. + */ + public function render_question_at_step($slot, $seq, $reviewing, $thispageurl = '') { + return $this->quba->render_question_at_step($slot, $seq, + $this->get_display_options($reviewing), + $this->quba->get_question($slot)->_number); + } + + /** + * Wrapper round print_question from lib/questionlib.php. + * + * @param integer $id the id of a question in this quiz attempt. + * @param boolean $reviewing is the being printed on an attempt or a review page. + * @param string $thispageurl the URL of the page this question is being printed on. + */ + public function render_question_for_commenting($slot) { + $options = $this->get_display_options(true); + $options->hide_all_feedback(); + $options->manualcomment = question_display_options::EDITABLE; + return $this->quba->render_question($slot, $options, $this->quba->get_question($slot)->_number); } - public function check_file_access($questionid, $isreviewing, $contextid, $component, + /** + * Check wheter access should be allowed to a particular file. + * + * @param integer $id the id of a question in this quiz attempt. + * @param boolean $reviewing is the being printed on an attempt or a review page. + * @param string $thispageurl the URL of the page this question is being printed on. + * @return string HTML for the question in its current state. + */ + public function check_file_access($questionid, $reviewing, $contextid, $component, $filearea, $args, $forcedownload) { - if ($isreviewing) { - $options = $this->get_review_options(); - } else { - $options = $this->get_render_options($questionid); - } - // XXX: mulitichoice type needs quiz id to get maxgrade - $options->quizid = $this->attempt->quiz; return question_check_file_access($this->questions[$questionid], - $this->get_question_state($questionid), $options, $contextid, - $component, $filearea, $args, $forcedownload); + $this->get_question_state($questionid), $this->get_display_options($reviewing), + $contextid, $component, $filearea, $args, $forcedownload); } /** * Triggers the sending of the notification emails at the end of this attempt. */ public function quiz_send_notification_emails() { - quiz_send_notification_emails($this->course, $this->quiz, $this->attempt, - $this->context, $this->cm); + quiz_send_notification_emails($this->get_course(), $this->get_quiz(), $this->attempt, + $this->quizobj->get_context(), $this->get_cm()); } /** @@ -856,7 +907,7 @@ class quiz_attempt extends quiz { * @return quiz_nav_panel_base the requested object. */ public function get_navigation_panel($panelclass, $page, $showall = false) { - $panel = new $panelclass($this, $this->get_review_options(), $page, $showall); + $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); return $panel->get_contents(); } @@ -869,7 +920,7 @@ class quiz_attempt extends quiz { */ public function links_to_other_attempts($url) { $search = '/\battempt=' . $this->attempt->id . '\b/'; - $attempts = quiz_get_user_attempts($this->quiz->id, $this->attempt->userid, 'all'); + $attempts = quiz_get_user_attempts($this->get_quiz()->id, $this->attempt->userid, 'all'); if (count($attempts) <= 1) { return false; } @@ -885,191 +936,129 @@ class quiz_attempt extends quiz { return implode(', ', $attemptlist); } - // Methods for processing manual comments ============================================== + // Methods for processing ================================================== + /** - * Process a manual comment for a question in this attempt. - * @param $questionid - * @param integer $questionid the question id - * @param string $comment the new comment from the teacher. - * @param mixed $grade the grade the teacher assigned, or '' to not change the grade. - * @return mixed true on success, a string error message if a problem is detected - * (for example score out of range). + * Process all the actions that were submitted as part of the current request. + * + * @param integer $timestamp the timestamp that should be stored as the modifed + * time in the database for these actions. If null, will use the current time. */ - public function process_comment($questionid, $comment, $commentformat, $grade) { - // I am not sure it is a good idea to have update methods here - this - // class is only about getting data out of the question engine, and - // helping to display it, apart from this. - $this->ensure_question_loaded($questionid); - $this->ensure_state_loaded($questionid); - $state = $this->states[$questionid]; - - $error = question_process_comment($this->questions[$questionid], - $state, $this->attempt, $comment, $commentformat, $grade); - - // If the state was update (successfully), save the changes. - if (!is_string($error) && $state->changed) { - if (!save_question_session($this->questions[$questionid], $state)) { - $error = get_string('errorudpatingquestionsession', 'quiz'); - } - if (!quiz_save_best_grade($this->quiz, $this->attempt->userid)) { - $error = get_string('errorudpatingbestgrade', 'quiz'); - } + public function process_all_actions($timestamp) { + $this->quba->process_all_actions($timestamp); + question_engine::save_questions_usage_by_activity($this->quba); + + $this->attempt->timemodified = $timestamp; + if ($this->attempt->timefinish) { + $this->attempt->sumgrades = $this->quba->get_total_mark(); + } + if (!update_record('quiz_attempts', $this->attempt)) { + throw new moodle_quiz_exception($this->get_quizobj(), 'saveattemptfailed'); + } + if (!$this->is_preview() && $this->attempt->timefinish) { + quiz_save_best_grade($this->get_quiz(), $this->get_userid()); } - return $error; } /** - * Print the fields of the comment form for questions in this attempt. - * @param $questionid a question id. - * @param $prefix Prefix to add to all field names. + * Update the flagged state for all question_attempts in this usage, if their + * flagged state was changed in the request. */ - public function question_print_comment_fields($questionid, $prefix) { - global $DB; + public function save_question_flags() { + $this->quba->update_question_flags(); + question_engine::save_questions_usage_by_activity($this->quba); + } - $this->ensure_question_loaded($questionid); - $this->ensure_state_loaded($questionid); + public function finish_attempt($timestamp) { + $this->quba->process_all_actions($timestamp); + $this->quba->finish_all_questions($timestamp); - /// Work out a nice title. - $student = $DB->get_record('user', array('id' => $this->get_userid())); - $a = new stdClass(); - $a->fullname = fullname($student, true); - $a->attempt = $this->get_attempt_number(); + question_engine::save_questions_usage_by_activity($this->quba); - question_print_comment_fields($this->questions[$questionid], - $this->states[$questionid], $prefix, $this->quiz, get_string('gradingattempt', 'quiz_grading', $a)); - } + $this->attempt->timemodified = $timestamp; + $this->attempt->timefinish = $timestamp; + $this->attempt->sumgrades = $this->quba->get_total_mark(); + if (!update_record('quiz_attempts', $this->attempt)) { + throw new moodle_quiz_exception($this->get_quizobj(), 'saveattemptfailed'); + } - // Private methods ===================================================================== - /** - * Check that the state of a particular question is loaded, and if not throw an exception. - * @param integer $id a question id. - */ - private function ensure_state_loaded($id) { - if (!array_key_exists($id, $this->states) || isset($this->states[$id]->_partiallyloaded)) { - throw new moodle_quiz_exception($this, 'statenotloaded', $id); + if (!$this->is_preview()) { + quiz_save_best_grade($this->get_quiz()); + $this->quiz_send_notification_emails(); } } /** - * @return string the layout of this quiz. Used by number_questions to - * work out which questions are on which pages. + * Print the fields of the comment form for questions in this attempt. + * @param $slot which question to output the fields for. + * @param $prefix Prefix to add to all field names. */ - protected function get_layout_string() { - return $this->attempt->layout; + public function question_print_comment_fields($slot, $prefix) { + // Work out a nice title. + $student = get_record('user', 'id', $this->get_userid()); + $a = new object(); + $a->fullname = fullname($student, true); + $a->attempt = $this->get_attempt_number(); + + question_print_comment_fields($this->quba->get_question_attempt($slot), + $prefix, $this->get_display_options(true)->markdp, + get_string('gradingattempt', 'quiz_grading', $a)); } + // Private methods ===================================================================== + /** * Get a URL for a particular question on a particular page of the quiz. * Used by {@link attempt_url()} and {@link review_url()}. * * @param string $script. Used in the URL like /mod/quiz/$script.php - * @param integer $questionid the id of a particular question on the page to jump to. 0 to just use the $page parameter. - * @param integer $page -1 to look up the page number from the questionid, otherwise the page number to go to. + * @param integer $slot identifies the specific question on the page to jump to. 0 to just use the $page parameter. + * @param integer $page -1 to look up the page number from the slot, otherwise the page number to go to. * @param boolean $showall if true, return a URL with showall=1, and not page number - * @param integer $thispage the page we are currently on. Links to questoins on this + * @param integer $thispage the page we are currently on. Links to questions on this * page will just be a fragment #q123. -1 to disable this. * @return The requested URL. */ - protected function page_and_question_url($script, $questionid, $page, $showall, $thispage) { - global $CFG; - + protected function page_and_question_url($script, $slot, $page, $showall, $thispage) { // Fix up $page if ($page == -1) { - if ($questionid && !$showall) { - $page = $this->questions[$questionid]->_page; + if ($slot && !$showall) { + $page = $this->quba->get_question($slot)->_page; } else { $page = 0; } } + if ($showall) { $page = 0; } - // Work out the correct start to the URL. - if ($thispage == $page) { - $url = ''; - } else { - $url = $CFG->wwwroot . '/mod/quiz/' . $script . '.php?attempt=' . $this->attempt->id; - if ($showall) { - $url .= '&showall=1'; - } else if ($page > 0) { - $url .= '&page=' . $page; - } - } - // Add a fragment to scroll down to the question. - if ($questionid) { - if ($questionid == reset($this->pagequestionids[$page])) { + $fragment = ''; + if ($slot) { + if ($slot == reset($this->pagelayout[$page])) { // First question on page, go to top. - $url .= '#'; + $fragment = '#'; } else { - $url .= '#q' . $questionid; + $fragment = '#q' . $slot; } } - return $url; - } -} - -/** - * A PHP Iterator for conviniently looping over the questions in a quiz. The keys are the question - * numbers (with 'i' for descriptions) and the values are the question objects. - * - * @copyright 2008 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - */ -class quiz_attempt_question_iterator implements Iterator { - private $attemptobj; // Reference to the quiz_attempt object we provide access to. - private $questionids; // Array of the question ids within that attempt we are iterating over. - - /** - * Constructor. Normally, you don't want to call this directly. Instead call - * quiz_attempt::get_question_iterator - * - * @param quiz_attempt $attemptobj the quiz_attempt object we will be providing access to. - * @param mixed $page as for @see{quiz_attempt::get_question_iterator}. - */ - public function __construct(quiz_attempt $attemptobj, $page = 'all') { - $this->attemptobj = $attemptobj; - $this->questionids = $attemptobj->get_question_ids($page); - } - - // Implementation of the Iterator interface ============================================ - 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; - } - } + // Work out the correct start to the URL. + if ($thispage == $page) { + return new moodle_url($fragment); - public function next() { - $id = next($this->questionids); - if ($id) { - return $this->attemptobj->get_question($id); } else { - return false; + $url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment, + array('attempt' => $this->attempt->id)); + if ($showall) { + $url->param('showall', 1); + } else if ($page > 0) { + $url->param('page', $page); + } + return $url; } } - - public function valid() { - return $this->current() !== false; - } } /** @@ -1090,7 +1079,8 @@ abstract class quiz_nav_panel_base { /** @var boolean */ protected $showall; - public function __construct(quiz_attempt $attemptobj, $options, $page, $showall) { + public function __construct(quiz_attempt $attemptobj, + question_display_options $options, $page, $showall) { $this->attemptobj = $attemptobj; $this->options = $options; $this->page = $page; @@ -1099,24 +1089,72 @@ abstract class quiz_nav_panel_base { protected function get_question_buttons() { $html = '
' . "\n"; - foreach ($this->attemptobj->get_question_iterator() as $number => $question) { - $html .= $this->get_question_button($number, $question) . "\n"; + foreach ($this->attemptobj->get_slots() as $slot) { + $qa = $this->attemptobj->get_question_attempt($slot); + $showcorrectness = $this->options->correctness && $qa->has_marks(); + $html .= $this->get_question_button($qa, $qa->get_question()->_number, + $showcorrectness) . "\n"; } $html .= "
\n"; return $html; } - protected function get_question_button($number, $question) { - $strstate = get_string($this->attemptobj->get_question_status($question->id), 'quiz'); - $flagstate = ''; - if ($this->attemptobj->is_question_flagged($question->id)) { - $flagstate = get_string('flagged', 'question'); + protected function get_button_id(question_attempt $qa) { + // The id to put on the button element in the HTML. + return 'quiznavbutton' . $qa->get_slot(); + } + + protected function get_question_button(question_attempt $qa, $number, $showcorrectness) { + $attributes = $this->get_attributes($qa, $showcorrectness); + + if (is_numeric($number)) { + $qnostring = 'questionnonav'; + } else { + $qnostring = 'questionnonavinfo'; + } + + $a = new stdClass; + $a->number = $number; + $a->attributes = implode(' ', $attributes); + + return '' . + '' . + get_string($qnostring, 'quiz', $a) . ''; + } + + /** + * @param question_attempt $qa + * @param boolean $showcorrectness + * @return array class name => descriptive string. + */ + protected function get_attributes(question_attempt $qa, $showcorrectness) { + // The current status of the question. + $attributes = array(); + + // On the current page? + if ($qa->get_question()->_page == $this->page) { + $attributes['thispage'] = get_string('onthispage', 'quiz'); + } + + // Question state. + $stateclass = $qa->get_state()->get_state_class($showcorrectness); + if (!$showcorrectness && $stateclass == 'notanswered') { + $stateclass = 'complete'; } - return '' . - $number . ' (' . $strstate . ' - ' . $flagstate . ')'; + $attributes[$stateclass] = $qa->get_state_string($showcorrectness); + + // Flagged? + if ($qa->is_flagged()) { + $attributes['flagged'] = '' . + get_string('flagged', 'question') . ''; + } else { + $attributes[''] = ''; + } + + return $attributes; } protected function get_before_button_bits() { @@ -1125,7 +1163,7 @@ abstract class quiz_nav_panel_base { abstract protected function get_end_bits(); - abstract protected function get_question_url($question); + abstract protected function get_question_url($slot); protected function get_user_picture() { global $DB, $OUTPUT; @@ -1138,28 +1176,12 @@ abstract class quiz_nav_panel_base { return $output; } - protected function get_question_state_classes($question) { - // The current status of the question. - $classes = $this->attemptobj->get_question_status($question->id); - - // Plus a marker for the current page. - if ($this->showall || $question->_page == $this->page) { - $classes .= ' thispage'; - } - - // Plus a marker for flagged questions. - if ($this->attemptobj->is_question_flagged($question->id)) { - $classes .= ' flagged'; - } - return $classes; - } - public function get_contents() { global $PAGE; $PAGE->requires->js_init_call('M.mod_quiz.nav.init', null, false, quiz_get_js_module()); $content = ''; - if ($this->attemptobj->get_quiz()->showuserpicture) { + if (!empty($this->attemptobj->get_quiz()->showuserpicture)) { $content .= $this->get_user_picture() . "\n"; } $content .= $this->get_before_button_bits(); @@ -1182,8 +1204,8 @@ abstract class quiz_nav_panel_base { * @since Moodle 2.0 */ class quiz_attempt_nav_panel extends quiz_nav_panel_base { - protected function get_question_url($question) { - return $this->attemptobj->attempt_url($question->id, -1, $this->page); + protected function get_question_url($slot) { + return $this->attemptobj->attempt_url($slot, -1, $this->page); } protected function get_before_button_bits() { @@ -1193,7 +1215,7 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base { protected function get_end_bits() { global $PAGE; $output = ''; - $output .= '' . get_string('finishattemptdots', 'quiz') . ''; + $output .= '' . get_string('endtest', 'quiz') . ''; $output .= $this->attemptobj->get_timer_html(); return $output; } @@ -1207,8 +1229,8 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base { * @since Moodle 2.0 */ class quiz_review_nav_panel extends quiz_nav_panel_base { - protected function get_question_url($question) { - return $this->attemptobj->review_url($question->id, -1, $this->showall, $this->page); + protected function get_question_url($slot) { + return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); } protected function get_end_bits() { diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index d0dcaeff493..2284f917d99 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -396,6 +396,7 @@ $string['incorrect'] = 'Incorrect'; $string['indivresp'] = 'Responses of individuals to each item'; $string['info'] = 'Info'; $string['infoshort'] = 'i'; +$string['inprogress'] = 'In progress'; $string['introduction'] = 'Introduction'; $string['invalidattemptid'] = 'No such attempt ID exists'; $string['invalidcategory'] = 'Category ID is invalid'; @@ -575,8 +576,8 @@ $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['questionnonav'] = 'Question {$a->number} {$a->attributes}'; -$string['questionnonavinfo'] = 'Information {$a->number} {$a->attributes}'; +$string['questionnonav'] = 'Question {$a->number} {$a->attributes}'; +$string['questionnonavinfo'] = 'Information {$a->number} {$a->attributes}'; $string['questionnotloaded'] = 'Question {$a} has not been loaded from the database'; $string['questionorder'] = 'Question order'; $string['questions'] = 'Questions'; diff --git a/mod/quiz/pix/navflagged.png b/mod/quiz/pix/navflagged.png new file mode 100644 index 0000000000000000000000000000000000000000..e1e10b7fbb8137f17b8d34e47894abbbac62dcdd GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFt3?wJp^Vk9@wg8_H*X<`x9bjPiJZYg7P=v80 w$S;_|;n|HeAcxn}#W93KHu=YaKg=wQ{3XnfR=U}}0!lD=y85}Sb4q9e05wJ#SpWb4 literal 0 HcmV?d00001 diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index ccd5739df60..d03a5d911eb 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -57,7 +57,7 @@ if (!confirm_sesskey()) { $PAGE->set_pagelayout('base'); // if no questions have been set up yet redirect to edit.php -if (!$quizobj->get_question_ids() && $quizobj->has_capability('mod/quiz:manage')) { +if (!$quizobj->has_questions() && $quizobj->has_capability('mod/quiz:manage')) { redirect($quizobj->edit_url()); } @@ -67,7 +67,6 @@ if ($quizobj->is_preview_user() && $forcenew) { $accessmanager->clear_password_access(); } - // Check capabilities. if (!$quizobj->is_preview_user()) { $quizobj->require_capability('mod/quiz:attempt'); @@ -186,10 +185,10 @@ if (!($quiz->attemptonlast && $lastattempt)) { } // Save the attempt in the database. -begin_sql(); +$transaction = $DB->start_delegated_transaction(); question_engine::save_questions_usage_by_activity($quba); $attempt->uniqueid = $quba->get_id(); -if (!$attempt->id = insert_record('quiz_attempts', $attempt)) { +if (!$attempt->id = $DB->insert_record('quiz_attempts', $attempt)) { throw new moodle_quiz_exception($quizobj, 'newattemptfail'); } @@ -212,7 +211,7 @@ $eventdata->user = $USER; $eventdata->attempt = $attempt->id; events_trigger('quiz_attempt_started', $eventdata); -commit_sql(); +$transaction->allow_commit(); // Redirect to the attempt page. redirect($quizobj->attempt_url($attempt->id)); diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index c264a4f5f93..2c7a35a3648 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -1,19 +1,72 @@ -.path-mod-quiz .graph.flexible-wrap {text-align:center;overflow:auto;} - -#page-mod-quiz-comment #manualgradingform, -#page-mod-quiz-report #manualgradingform {width: 100%;} - -/** Mixed **/ -#page-mod-quiz-attempt .submitbtns, -#page-mod-quiz-review .submitbtns, -#page-mod-quiz-summary .submitbtns {text-align: left;margin-top: 1.5em;} - +/** Attempt and review pages **/ #page-mod-quiz-attempt #page .controls, #page-mod-quiz-summary #page .controls, #page-mod-quiz-review #page .controls {text-align: center;margin: 8px auto;} +#page-mod-quiz-attempt .submitbtns, +#page-mod-quiz-review .submitbtns {clear: left; text-align: left; padding-top: 1.5em;} + body.jsenabled .questionflagcheckbox {display: none;} +/* Question navigation block. */ +#quiznojswarning {color: red;} +#quiznojswarning {font-size: 0.7em;line-height: 1.1;} +.jsenabled #quiznojswarning {display: none;} + +.path-mod-quiz #user-picture {margin: 0.5em 0;} +.path-mod-quiz #user-picture img {width: auto;height: auto;float: left;} + +.path-mod-quiz .qnbutton {display: block; position: relative; float: left; width: 1.5em; height: 1.5em; overflow: hidden; margin: 0.3em 0.3em 0.3em 0; padding: 0; border: 1px solid #bbb; background: #ddd; text-align: center; vertical-align: middle;line-height: 1.5em !important; font-weight: bold; text-decoration: none;} +.path-mod-quiz .qnbutton:hover {text-decoration: underline;} +.path-mod-quiz .qnbutton span {cursor: pointer; cursor: hand;} + +.path-mod-quiz .qnbutton .trafficlight, +.path-mod-quiz .qnbutton .thispageholder {display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0;} + +.path-mod-quiz .qnbutton.thispage {border-color: #666;} +.path-mod-quiz .qnbutton.thispage .thispageholder {border: 1px solid #666;} + +.path-mod-quiz .qnbutton.flagged .trafficlight {background: url([[pix:quiz|navflagged]]) no-repeat top right;} + +.path-mod-quiz .qnbutton.notyetanswered, +.path-mod-quiz .qnbutton.requiresgrading, +.path-mod-quiz .qnbutton.invalidanswer {background-color: white;} +.path-mod-quiz .qnbutton.correct {background-color: #cfc;} +.path-mod-quiz .qnbutton.correct .trafficlight {border-bottom: 3px solid #080;} +.path-mod-quiz .qnbutton.partiallycorrect {background-color: #ffa;} +.path-mod-quiz .qnbutton.notanswered, +.path-mod-quiz .qnbutton.incorrect {background-color: #fcc;} +.path-mod-quiz .qnbutton.notanswered .trafficlight, +.path-mod-quiz .qnbutton.incorrect .trafficlight {border-top: 3px solid #800;} + +.path-mod-quiz .othernav {clear: both; margin: 0.5em 0;} +.path-mod-quiz .othernav a, +.path-mod-quiz .othernav input {display: block;margin: 0.5em 0;} + +/* Countdown timer. */ +#quiz-timer {display: none; margin-top: 1em;} +#quiz-time-left {font-weight: bold;} +#quiz-timer.timeleft15 {background: #ffffff;} +#quiz-timer.timeleft14 {background: #ffeeee;} +#quiz-timer.timeleft13 {background: #ffdddd;} +#quiz-timer.timeleft12 {background: #ffcccc;} +#quiz-timer.timeleft11 {background: #ffbbbb;} +#quiz-timer.timeleft10 {background: #ffaaaa;} +#quiz-timer.timeleft9 {background: #ff9999;} +#quiz-timer.timeleft8 {background: #ff8888;} +#quiz-timer.timeleft7 {background: #ff7777;} +#quiz-timer.timeleft6 {background: #ff6666;} +#quiz-timer.timeleft5 {background: #ff5555;} +#quiz-timer.timeleft4 {background: #ff4444;} +#quiz-timer.timeleft3 {background: #ff3333;} +#quiz-timer.timeleft2 {background: #ff2222;} +#quiz-timer.timeleft1 {background: #ff1111;} +#quiz-timer.timeleft0 {background: #ff0000;} + +/** mod quiz mod **/ +#page-mod-quiz-mod #reviewoptionshdr .fitem {width: 23%;margin-left: 10px;} +#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {width: 100%;text-align: left;margin-left: 0;} + #page-mod-quiz-edit div.question div.content .questiontext, #categoryquestions .questiontext {-o-text-overflow:ellipsis;text-overflow:ellipsis;position:relative;zoom:1;padding-left:0.3em;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} @@ -41,13 +94,6 @@ div.editq div.question div.content .singlequestion a .questiontext{text-decorati #page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span label, #adminquizreviewoptions span label {margin-left: 0.4em;} -table#categoryquestions td, -#page-mod-quiz-edit table#categoryquestions th{overflow:hidden;white-space:nowrap;} - -/** mod quiz mod **/ -#page-mod-quiz-mod #reviewoptionshdr .fitem {width: 30%;margin-left: 10px;} -#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {width: 100%;text-align: left;margin-left: 0;} - /** Mod quiz view **/ #page-mod-quiz-view .quizinfo, #page-mod-quiz-view #page .quizgradefeedback, @@ -60,37 +106,28 @@ table#categoryquestions td, .quizstartbuttondiv.quizsecuremoderequired input { display: none; } .jsenabled .quizstartbuttondiv.quizsecuremoderequired input { display: inline; } +.mod-quiz .gradedattempt, +.mod-quiz tr.gradedattempt td { background-color: #e8e8e8; } + /** Mod quiz summary **/ #page-mod-quiz-summary #content {text-align: center;} #page-mod-quiz-summary .questionflag {width: 16px;height: 16px;vertical-align: middle;} -#page-mod-quiz-summary #quiz-timer {text-align: center;} +#page-mod-quiz-summary #quiz-timer {text-align: center; margin-top: 1em;} +#page-mod-quiz-summary .submitbtns {margin-top: 1.5em;} @media print { .quiz-secure-window * { display: none !important; } } -/** Countdown timer. */ -#quiz-timer {display: none; margin-top: 1em;} -#quiz-time-left {font-weight: bold;} -#quiz-timer.timeleft15 {background: #ffffff;} -#quiz-timer.timeleft14 {background: #ffeeee;} -#quiz-timer.timeleft13 {background: #ffdddd;} -#quiz-timer.timeleft12 {background: #ffcccc;} -#quiz-timer.timeleft11 {background: #ffbbbb;} -#quiz-timer.timeleft10 {background: #ffaaaa;} -#quiz-timer.timeleft9 {background: #ff9999;} -#quiz-timer.timeleft8 {background: #ff8888;} -#quiz-timer.timeleft7 {background: #ff7777;} -#quiz-timer.timeleft6 {background: #ff6666;} -#quiz-timer.timeleft5 {background: #ff5555;} -#quiz-timer.timeleft4 {background: #ff4444;} -#quiz-timer.timeleft3 {background: #ff3333;} -#quiz-timer.timeleft2 {background: #ff2222;} -#quiz-timer.timeleft1 {background: #ff1111;} -#quiz-timer.timeleft0 {background: #ff0000;} - /** Mod quiz review **/ -#page-mod-quiz-review .pagingbar {margin: 1.5em auto;} +table.quizreviewsummary {width: 100%;} +table.quizreviewsummary th.cell {padding: 1px 0.5em 1px 1em;font-weight: bold;text-align: right;width: 10em;background: #f0f0f0;} +table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;background: #fafafa;} + +/** Mod quiz make comment or override grade popup. **/ +#page-mod-quiz-comment .mform {width: 100%;} +#page-mod-quiz-comment .mform fieldset {margin: 0;} +#page-mod-quiz-comment .que {margin: 0;} /** Mod quiz report **/ #page-mod-quiz-report h2.main {clear: both;} @@ -99,9 +136,17 @@ table#categoryquestions td, #page-mod-quiz-report .dubious{background-color: #fcc;} #page-mod-quiz-report .highlight{border :medium solid yellow;background-color:lightYellow;} #page-mod-quiz-report .negcovar{border :medium solid pink;} +#page-mod-quiz-report .toggleincludeauto {text-align: center;} +#page-mod-quiz-report .gradetheselink {font-size: 0.8em;} +#page-mod-quiz-report #manualgradingform {width: 100%;} +#page-mod-quiz-report #manualgradingform.mform br {clear: none;} +#page-mod-quiz-report #manualgradingform.mform .clearfix:after {clear: none;} #page-mod-quiz-report #manualgradingform .que {margin-bottom: 0.7em;} -#page-mod-quiz-report table.titlesleft td.c0{font-weight: bold;} +#page-mod-quiz-report .mform fieldset {margin: 0;} +#page-mod-quiz-report fieldset.felement.fgroup {margin: 0;} +#page-mod-quiz-report table.titlesleft td.c0 {font-weight: bold;} #page-mod-quiz-report table .numcol {text-align: center;vertical-align : middle !important;} + #page-mod-quiz-report table#attempts {clear: both;width: 80%; margin: 0.2em auto;} #page-mod-quiz-report table#attempts .header, #page-mod-quiz-report table#attempts .cell{padding: 4px;} @@ -110,16 +155,13 @@ table#categoryquestions td, #page-mod-quiz-report table#attempts td {border-left-width: 1px;border-right-width: 1px;border-left-style: solid;border-right-style: solid;vertical-align: middle;} #page-mod-quiz-report table#attempts .header {text-align: left;} #page-mod-quiz-report table#attempts .picture {text-align: center !important;} -#page-mod-quiz-report table#itemanalysis {width: 80%;margin: 20px auto;} -#page-mod-quiz-report table#itemanalysis td {border-width: 1px;border-style: solid;} -#page-mod-quiz-report table#itemanalysis .header {text-align: left;padding: 4px;} -#page-mod-quiz-report table#itemanalysis .header .commands {display: inline;} -#page-mod-quiz-report table#itemanalysis .uncorrect {color: red;} -#page-mod-quiz-report table#itemanalysis .correct {color: blue;font-weight : bold;} -#page-mod-quiz-report table#itemanalysis .partialcorrect {color: green !important;} -#page-mod-quiz-report table#itemanalysis .cell{padding: 4px;} -#page-mod-quiz-report table#itemanalysis .qname {color: green !important;} -#page-mod-quiz-report fieldset.felement.fgroup {margin: 0;} +#page-mod-quiz-report table#attempts.grades span.que, +#page-mod-quiz-report table#attempts span.avgcell {white-space: nowrap;} +#page-mod-quiz-report table#attempts span.que .requiresgrading {white-space: normal;} +#page-mod-quiz-report table#attempts .questionflag {width: 16px; height: 16px; vertical-align: middle;} + +#page-mod-quiz-report .graph.flexible-wrap {text-align:center; overflow:auto;} + #page-mod-quiz-report #cachingnotice {margin-bottom: 1em; padding: 0.2em; } #page-mod-quiz-report #cachingnotice .singlebutton {margin: 0.5em 0 0;} #page-mod-quiz-report .bold .reviewlink {font-weight: normal;} @@ -133,79 +175,26 @@ table#categoryquestions td, #page-mod-quiz-grading table#grading td {border-left-width: 1px;border-right-width: 1px;border-left-style: solid;border-right-style: solid;vertical-align: bottom;} /** Mod quiz attempt **/ -#categoryquestions .r1 {background: #e4e4e4;} -#categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;} -#categoryquestions th.modifiername .sorters, -#categoryquestions th.creatorname .sorters {font-weight: normal;font-size: 0.8em;} -table#categoryquestions {width: 100%;overflow: hidden;table-layout: fixed;} -#categoryquestions .iconcol {width: 15px;text-align: center;padding: 0;} -#categoryquestions .checkbox {width: 19px;text-align: center;padding: 0;} -#categoryquestions .qtype {text-align: center;} -#categoryquestions .qtype {width: 24px;padding: 0;} -#categoryquestions .questiontext p {margin: 0;} - table.quizattemptsummary .bestrow td {background-color: #e8e8e8;} table.quizattemptsummary .noreviewmessage {color: gray;} -table.quizreviewsummary {width: 100%;} -table.quizreviewsummary th.cell {padding: 1px 0.5em 1px 1em;font-weight: bold;text-align: right;width: 10em;background: #f0f0f0;} -table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;background: #fafafa;} - -.path-mod-quiz #user-picture {margin: 0.5em 0;} -.path-mod-quiz #user-picture img {width: auto;height: auto;float: left;} -.path-mod-quiz .othernav {clear: both;} -.path-mod-quiz .othernav a, -.path-mod-quiz .othernav input {display: block;margin: 0.5em 0;} -.path-mod-quiz .qnbutton {display: block;float: left;width: 1.5em;height: 1.5em;overflow: hidden;margin: 0.3em 0.3em 0.3em 0;padding: 0;border: 1px solid #bbb;background: #eee no-repeat top right;text-align: center;vertical-align: middle;cursor: pointer;white-space: normal;font: inherit;line-height: 1.5em;font-weight: bold;color: #00f;border-color: #bbb;background-color: #ddd;} -.path-mod-quiz .qnbutton:hover {text-decoration: underline;color: #f00;} -.path-mod-quiz .qnbutton.flagged {background-image: url([[pix:i/ne_red_mark]]);} -.path-mod-quiz .qnbutton.thispage {border-color: black;} -.path-mod-quiz .qnbutton.open {background-color: white;} -.path-mod-quiz .qnbutton.correct {background-color: #cfc;} -.path-mod-quiz .qnbutton.partiallycorrect {background-color: #ffa;} -.path-mod-quiz .qnbutton.incorrect {background-color: #fcc;} - -#quiznojswarning {color: red;} -#quiznojswarning {font-size: 0.7em;line-height: 1.1;} -.jsenabled #quiznojswarning {display: none;} - -body#question-preview .quemodname, -body#question-preview .controls{text-align: center;} - .quizattemptcounts {clear: left; text-align: center;} .generalbox#passwordbox { /* Should probably match .generalbox#intro above */width:70%;margin-left:auto;margin-right:auto;} #passwordform {margin: 1em 0;} -.questionbankwindow.block {float:right;width:30%;right:0.3em;padding-bottom:0.5em;display:block;border-width:0;} -.questionbankwindow.block .content {padding:0;} -.questionbankwindow .choosecategory, -.questionbankwindow .createnewquestion {padding: 0.3em;} -.questionbankwindow .createnewquestion .singlebutton {display: inline;} -.questionbankwindow #catmenu_jump {display: block;} - -.questionbank div.categoryquestionscontainer, -.questionbank .categorysortopotionscontainer, -.questionbank .categorypagingbarcontainer, -.questionbank .categoryselectallcontainer{padding-left:0.3em;padding-right:0.3em;} - -.noquestionsincategory{clear:both;padding-top:1em;padding-bottom:1em;} -.modulespecificbuttonscontainer{padding-left:0.3em;padding-right:0.3em;} - -#adminquizreviewoptions {margin-bottom: 0.5em;} -.quizquestionlistcontrols {text-align: center;} - -.categoryinfo {padding: 0.3em;} - -.path-mod-quiz .gradingdetails {font-size: small;} -.path-mod-quiz .highlightgraded {background:yellow;} -.path-mod-quiz div.tabtree a span img.iconsmall {vertical-align: baseline;} -.ie6.path-mod-quiz div.tabtree a span img.iconsmall {margin: 0;vertical-align: baseline;position: relative;top: 1px;} -.ie7.path-mod-quiz div.tabtree a span img.iconsmall {margin: 0;vertical-align: baseline;position: relative;top: 2px;} - /** Mod quiz edit **/ #page-mod-quiz-edit h2.main{display:inline;padding-right:1em;clear:left;} - +#categoryquestions .r1 {background: #e4e4e4;} +#categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;} +#categoryquestions th.modifiername .sorters, +#categoryquestions th.creatorname .sorters {font-weight: normal;font-size: 0.8em;} +table#categoryquestions {width: 100%;overflow: hidden;table-layout: fixed;} +#categoryquestions .iconcol {width: 15px;text-align: center;padding: 0;} +#categoryquestions .checkbox {width: 19px;text-align: center;padding: 0;} +#categoryquestions .qtype {text-align: center;} +#categoryquestions .qtype {width: 24px;padding: 0;} +#categoryquestions .questiontext p {margin: 0;} #page-mod-quiz-edit div.quizcontents {float:left;width:70%;display:block;clear:left;} #page-mod-quiz-edit div.quizwhenbankcollapsed {width:100%;} @@ -287,6 +276,30 @@ body#question-preview .controls{text-align: center;} #page-mod-quiz-edit .editq div.question div.description div.content .questiontext {max-width: 75%;} #page-mod-quiz-edit .editq div.question div.qnum{font-size:1.5em;} +table#categoryquestions td, +#page-mod-quiz-edit table#categoryquestions th{overflow:hidden;white-space:nowrap;} + +.questionbankwindow.block {float:right;width:30%;right:0.3em;padding-bottom:0.5em;display:block;border-width:0;} +.questionbankwindow.block .content {padding:0;} +.questionbankwindow .choosecategory, +.questionbankwindow .createnewquestion {padding: 0.3em;} +.questionbankwindow .createnewquestion .singlebutton {display: inline;} +.questionbankwindow #catmenu_jump {display: block;} + +.questionbank div.categoryquestionscontainer, +.questionbank .categorysortopotionscontainer, +.questionbank .categorypagingbarcontainer, +.questionbank .categoryselectallcontainer{padding-left:0.3em;padding-right:0.3em;} + +.noquestionsincategory{clear:both;padding-top:1em;padding-bottom:1em;} +.modulespecificbuttonscontainer{padding-left:0.3em;padding-right:0.3em;} + +.quizquestionlistcontrols {text-align: center;} + +.categoryinfo {padding: 0.3em;} + +.path-mod-quiz .gradingdetails {font-size: small;} + body #quizcontentsblock #repaginatedialog {display: none;} body.jsenabled #quizcontentsblock #repaginatedialog .hd {display:block;} body.jsenabled #quizcontentsblock #repaginatedialog .bd {padding:1em;} @@ -374,4 +387,7 @@ bank window's title is prominent enough*/ .ie6#page-mod-quiz-edit div.question div.content .questiontext {width:50%;} .ie6#page-mod-quiz-edit div.question div.content .questionname {width:20%;} .ie6#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory a{width:40%;} -.ie6#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory label{width: 35%;} \ No newline at end of file +.ie6#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory label{width: 35%;} + +/** settings.php */ +#adminquizreviewoptions {margin-bottom: 0.5em;} diff --git a/question/engine/lib.php b/question/engine/lib.php index 53112325210..747e79cdbf6 100644 --- a/question/engine/lib.php +++ b/question/engine/lib.php @@ -740,7 +740,7 @@ class question_usage_by_activity { } /** @return array all the identifying numbers of all the questions in this usage. */ - public function get_question_numbers() { + public function get_slots() { return array_keys($this->questionattempts); } @@ -1016,7 +1016,7 @@ class question_usage_by_activity { public function process_all_actions($timestamp = null, $postdata = null) { $slots = question_attempt::get_submitted_var('slots', PARAM_SEQUENCE, $postdata); if (is_null($slots)) { - $slots = $this->get_question_numbers(); + $slots = $this->get_slots(); } else if (!$slots) { $slots = array(); } else { @@ -1239,7 +1239,7 @@ class question_attempt_iterator implements Iterator, ArrayAccess { */ public function __construct(question_usage_by_activity $quba) { $this->quba = $quba; - $this->slots = $quba->get_question_numbers(); + $this->slots = $quba->get_slots(); $this->rewind(); } diff --git a/question/todo/diffstat.txt b/question/todo/diffstat.txt index ed64c923c98..9386dda5bc0 100644 --- a/question/todo/diffstat.txt +++ b/question/todo/diffstat.txt @@ -44,19 +44,19 @@ Internal changes admin/report/quizupgrade/resetquiz.php | 70 + admin/report/quizupgrade/settings.php | 5 + - lang/en_utf8/help/question/generalfeedback.html | 14 + - lang/en_utf8/help/question/howquestionsbehave.html | 13 + - lang/en_utf8/help/question/penalty.html | 11 + - lang/en_utf8/help/question/questiontext.html | 7 + +DONE lang/en_utf8/help/question/generalfeedback.html | 14 + +DONE lang/en_utf8/help/question/howquestionsbehave.html | 13 + +DONE lang/en_utf8/help/question/penalty.html | 11 + +DONE lang/en_utf8/help/question/questiontext.html | 7 + lang/en_utf8/help/quiz/decimalplacesquestion.html | 6 + lang/en_utf8/help/quiz/showuserpicture.html | 7 + - lang/en_utf8/qtype_match.php | 7 - - lang/en_utf8/qtype_multichoice.php | 34 - - lang/en_utf8/qtype_numerical.php | 10 - - lang/en_utf8/qtype_random.php | 4 - - lang/en_utf8/qtype_shortanswer.php | 6 - - lang/en_utf8/qtype_truefalse.php | 13 - - lang/en_utf8/question.php | 104 +- +DONE lang/en_utf8/qtype_match.php | 7 - +DONE lang/en_utf8/qtype_multichoice.php | 34 - +DONE lang/en_utf8/qtype_numerical.php | 10 - +DONE lang/en_utf8/qtype_random.php | 4 - +DONE lang/en_utf8/qtype_shortanswer.php | 6 - +DONE lang/en_utf8/qtype_truefalse.php | 13 - +DONE lang/en_utf8/question.php | 104 +- lang/en_utf8/quiz.php | 207 ++- lang/en_utf8/quiz_grading.php | 18 - lang/en_utf8/quiz_overview.php | 30 - @@ -68,7 +68,7 @@ DONE lib/questionlib.php | 1434 ++-------- DONE mod/quiz/accessrules.php | 828 ++++++ DONE mod/quiz/attempt.php | 742 ++---- DONE mod/quiz/attempt_close_js.php | 27 - - mod/quiz/attemptlib.php | 1219 +++++++++ +DONE mod/quiz/attemptlib.php | 1219 +++++++++ mod/quiz/backuplib.php | 16 +- mod/quiz/comment.php | 171 +- mod/quiz/config.html | 304 ++- @@ -160,9 +160,9 @@ DONE mod/quiz/version.php | 6 +- mod/quiz/report/statistics/statistics_table.php | 335 +++ mod/quiz/report/statistics/version.php | 26 + - theme/standard/styles_color.css | 72 +- - theme/standard/styles_fonts.css | 20 +- - theme/standard/styles_layout.css | 265 ++- +DONE theme/standard/styles_color.css | 72 +- +DONE theme/standard/styles_fonts.css | 20 +- +DONE theme/standard/styles_layout.css | 265 ++- DONE pix/i/flagged.png | Bin 0 -> 193 bytes DONE pix/i/ne_red_mark.png | Bin 0 -> 121 bytes -- 2.43.0