moodle/mod/quiz/locallib.php
Tim Hunt d04f623be4 quiz editing MDL-23705 the edit icon after the question name disappears some times.
It is worse with some themes and some browsers.
You can still edit by clicking the question name, but that is not obvious.
I moved the edit icon to the left.
2010-08-11 19:38:16 +00:00

1269 lines
46 KiB
PHP

<?php
///////////////////////////////////////////////////////////////////////////
// //
// NOTICE OF COPYRIGHT //
// //
// Moodle - Modular Object-Oriented Dynamic Learning Environment //
// http://moodle.org //
// //
// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation; either version 2 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License for more details: //
// //
// http://www.gnu.org/copyleft/gpl.html //
// //
///////////////////////////////////////////////////////////////////////////
/**
* Library of functions used by the quiz module.
*
* This contains functions that are called from within the quiz module only
* Functions that are also called by core Moodle are in {@link lib.php}
* This script also loads the code in {@link questionlib.php} which holds
* the module-indpendent code for handling questions and which in turn
* initialises all the questiontype classes.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package quiz
*/
if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
}
/**
* Include those library functions that are also used by core Moodle or other modules
*/
require_once($CFG->dirroot . '/mod/quiz/lib.php');
require_once($CFG->dirroot . '/mod/quiz/accessrules.php');
require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->libdir . '/eventslib.php');
/// Constants ///////////////////////////////////////////////////////////////////
/**#@+
* Constants to describe the various states a quiz attempt can be in.
*/
define('QUIZ_STATE_DURING', 'during');
define('QUIZ_STATE_IMMEDIATELY', 'immedately');
define('QUIZ_STATE_OPEN', 'open');
define('QUIZ_STATE_CLOSED', 'closed');
define('QUIZ_STATE_TEACHERACCESS', 'teacheraccess'); // State only relevant if you are in a studenty role.
/**#@-*/
/**
* We show the countdown timer if there is less than this amount of time left before the
* the quiz close date. (1 hour)
*/
define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600');
/// Functions related to attempts /////////////////////////////////////////
/**
* Creates an object to represent a new attempt at a quiz
*
* Creates an attempt object to represent an attempt at the quiz by the current
* user starting at the current time. The ->id field is not set. The object is
* NOT written to the database.
*
* @param object $quiz the quiz to create an attempt for.
* @param integer $attemptnumber the sequence number for the attempt.
* @param object $lastattempt the previous attempt by this user, if any. Only needed
* if $attemptnumber > 1 and $quiz->attemptonlast is true.
* @param integer $timenow the time the attempt was started at.
* @param boolean $ispreview whether this new attempt is a preview.
*
* @return object the newly created attempt object.
*/
function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) {
global $USER;
if ($attemptnumber == 1 || !$quiz->attemptonlast) {
/// We are not building on last attempt so create a new attempt.
$attempt = new stdClass;
$attempt->quiz = $quiz->id;
$attempt->userid = $USER->id;
$attempt->preview = 0;
if ($quiz->shufflequestions) {
$attempt->layout = quiz_clean_layout(quiz_repaginate($quiz->questions, $quiz->questionsperpage, true),true);
} else {
$attempt->layout = quiz_clean_layout($quiz->questions,true);
}
} else {
/// Build on last attempt.
if (empty($lastattempt)) {
print_error('cannotfindprevattempt', 'quiz');
}
$attempt = $lastattempt;
}
$attempt->attempt = $attemptnumber;
$attempt->sumgrades = 0.0;
$attempt->timestart = $timenow;
$attempt->timefinish = 0;
$attempt->timemodified = $timenow;
$attempt->uniqueid = question_new_attempt_uniqueid();
/// If this is a preview, mark it as such.
if ($ispreview) {
$attempt->preview = 1;
}
return $attempt;
}
/**
* Returns the unfinished attempt for the given
* user on the given quiz, if there is one.
*
* @param integer $quizid the id of the quiz.
* @param integer $userid the id of the user.
*
* @return mixed the unfinished attempt if there is one, false if not.
*/
function quiz_get_user_attempt_unfinished($quizid, $userid) {
$attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
if ($attempts) {
return array_shift($attempts);
} else {
return false;
}
}
/**
* Returns the most recent attempt by a given user on a given quiz.
* May be finished, or may not.
*
* @param integer $quizid the id of the quiz.
* @param integer $userid the id of the user.
*
* @return mixed the attempt if there is one, false if not.
*/
function quiz_get_latest_attempt_by_user($quizid, $userid) {
global $CFG, $DB;
$attempt = $DB->get_records_sql('SELECT qa.* FROM {quiz_attempts} qa
WHERE qa.quiz=? AND qa.userid= ? ORDER BY qa.timestart DESC, qa.id DESC', array($quizid, $userid), 0, 1);
if ($attempt) {
return array_shift($attempt);
} else {
return false;
}
}
/**
* Load an attempt by id. You need to use this method instead of $DB->get_record, because
* of some ancient history to do with the upgrade from Moodle 1.4 to 1.5, See the comment
* after CREATE TABLE `prefix_quiz_newest_states` in mod/quiz/db/mysql.php.
*
* @param integer $attemptid the id of the attempt to load.
*/
function quiz_load_attempt($attemptid) {
global $DB;
$attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
if (!$attempt) {
return false;
}
if (!$DB->record_exists('question_sessions', array('attemptid' => $attempt->uniqueid))) {
/// this attempt has not yet been upgraded to the new model
quiz_upgrade_states($attempt);
}
return $attempt;
}
/**
* Delete a quiz attempt.
* @param mixed $attempt an integer attempt id or an attempt object (row of the quiz_attempts table).
* @param object $quiz the quiz object.
*/
function quiz_delete_attempt($attempt, $quiz) {
global $DB;
if (is_numeric($attempt)) {
if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) {
return;
}
}
if ($attempt->quiz != $quiz->id) {
debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " .
"but was passed quiz $quiz->id.");
return;
}
$DB->delete_records('quiz_attempts', array('id' => $attempt->id));
delete_attempt($attempt->uniqueid);
// Search quiz_attempts for other instances by this user.
// If none, then delete record for this quiz, this user from quiz_grades
// else recalculate best grade
$userid = $attempt->userid;
if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) {
$DB->delete_records('quiz_grades', array('userid' => $userid,'quiz' => $quiz->id));
} else {
quiz_save_best_grade($quiz, $userid);
}
quiz_update_grades($quiz, $userid);
}
/**
* Delete all the preview attempts at a quiz, or possibly all the attempts belonging
* to one user.
* @param object $quiz the quiz object.
* @param integer $userid (optional) if given, only delete the previews belonging to this user.
*/
function quiz_delete_previews($quiz, $userid = null) {
global $DB;
$conditions = array('quiz' => $quiz->id, 'preview' => 1);
if (!empty($userid)) {
$conditions['userid'] = $userid;
}
$previewattempts = $DB->get_records('quiz_attempts', $conditions);
foreach ($previewattempts as $attempt) {
quiz_delete_attempt($attempt, $quiz);
}
}
/**
* @param integer $quizid The quiz id.
* @return boolean whether this quiz has any (non-preview) attempts.
*/
function quiz_has_attempts($quizid) {
global $DB;
return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0));
}
/// Functions to do with quiz layout and pages ////////////////////////////////
/**
* Returns a comma separated list of question ids for the current page
*
* @param string $layout the string representing the quiz layout. Each page is represented as a
* comma separated list of question ids and 0 indicating page breaks.
* So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
* @param integer $page the number of the current page.
* @return string comma separated list of question ids
*/
function quiz_questions_on_page($layout, $page) {
$pages = explode(',0', $layout);
return trim($pages[$page], ',');
}
/**
* Returns a comma separated list of question ids for the quiz
*
* @param string $layout The string representing the quiz layout. Each page is
* represented as a comma separated list of question ids and 0 indicating
* page breaks. So 5,2,0,3,0 means questions 5 and 2 on page 1 and question
* 3 on page 2
* @return string comma separated list of question ids, without page breaks.
*/
function quiz_questions_in_quiz($layout) {
$layout = preg_replace('/,(0+,)+/', ',', $layout); // Remove page breaks from the middle.
$layout = preg_replace('/^0+,/', '', $layout); // And from the start.
$layout = preg_replace('/(^|,)0+$/', '', $layout); // And from the end.
return $layout;
}
/**
* Returns the number of pages in a quiz layout
*
* @param string $layout The string representing the quiz layout. Always ends in ,0
* @return integer The number of pages in the quiz.
*/
function quiz_number_of_pages($layout) {
$count = 0;
if ($layout !== '') {
//if the first page is empty, include it, too
if (strcmp($layout[0], '0') === 0) {
$count++;
}
$count += substr_count($layout, ',0');
}
return $count;
}
/**
* Returns the number of questions in the quiz layout
*
* @param string $layout the string representing the quiz layout.
* @return integer The number of questions in the quiz.
*/
function quiz_number_of_questions_in_quiz($layout) {
$layout = quiz_questions_in_quiz(quiz_clean_layout($layout));
$count = substr_count($layout, ',');
if ($layout !== '') {
$count++;
}
return $count;
}
/**
* Returns the first question number for the current quiz page
*
* @param string $quizlayout The string representing the layout for the whole quiz
* @param string $pagelayout The string representing the layout for the current page
* @return integer the number of the first question
*/
function quiz_first_questionnumber($quizlayout, $pagelayout) {
// this works by finding all the questions from the quizlayout that
// come before the current page and then adding up their lengths.
global $CFG, $DB;
$start = strpos($quizlayout, ','.$pagelayout.',')-2;
if ($start > 0) {
$prevlist = substr($quizlayout, 0, $start);
list($usql, $params) = $DB->get_in_or_equal(explode(',', $prevlist));
return $DB->get_field_sql("SELECT sum(length)+1 FROM {question}
WHERE id $usql", $params);
} else {
return 1;
}
}
/**
* Re-paginates the quiz layout
*
* @param string $layout The string representing the quiz layout.
* @param integer $perpage The number of questions per page
* @param boolean $shuffle Should the questions be reordered randomly?
* @return string the new layout string
*/
function quiz_repaginate($layout, $perpage, $shuffle = false) {
$layout = str_replace(',0', '', $layout); // remove existing page breaks
$questions = explode(',', $layout);
//remove empty pages from beginning
while (reset($questions) == '0') {
array_shift($questions);
}
if ($shuffle) {
shuffle($questions);
}
$i = 1;
$layout = '';
foreach ($questions as $question) {
if ($perpage and $i > $perpage) {
$layout .= '0,';
$i = 1;
}
$layout .= $question.',';
$i++;
}
return $layout.'0';
}
/// Functions to do with quiz grades //////////////////////////////////////////
/**
* Creates an array of maximum grades for a quiz
* The grades are extracted from the quiz_question_instances table.
*
* @param integer $quiz The quiz object
* @return array Array of grades indexed by question id. These are the maximum
* possible grades that students can achieve for each of the questions.
*/
function quiz_get_all_question_grades($quiz) {
global $CFG, $DB;
$questionlist = quiz_questions_in_quiz($quiz->questions);
if (empty($questionlist)) {
return array();
}
$params = array($quiz->id);
$wheresql = '';
if (!is_null($questionlist)) {
list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist));
$wheresql = " AND question $usql ";
$params = array_merge($params, $question_params);
}
$instances = $DB->get_records_sql("SELECT question,grade,id
FROM {quiz_question_instances}
WHERE quiz = ? $wheresql", $params);
$list = explode(",", $questionlist);
$grades = array();
foreach ($list as $qid) {
if (isset($instances[$qid])) {
$grades[$qid] = $instances[$qid]->grade;
} else {
$grades[$qid] = 1;
}
}
return $grades;
}
/**
* Update the sumgrades field of the quiz. This needs to be called whenever
* the grading structure of the quiz is changed. For example if a question is
* added or removed, or a question weight is changed.
*
* @param object $quiz a quiz.
*/
function quiz_update_sumgrades($quiz) {
global $DB;
$grades = quiz_get_all_question_grades($quiz);
$sumgrades = 0;
foreach ($grades as $grade) {
$sumgrades += $grade;
}
if (!isset($quiz->sumgrades) || $quiz->sumgrades != $sumgrades) {
$DB->set_field('quiz', 'sumgrades', $sumgrades, array('id' => $quiz->id));
$quiz->sumgrades = $sumgrades;
}
}
/**
* Convert the raw grade stored in $attempt into a grade out of the maximum
* grade for this quiz.
*
* @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
* @param object $quiz the quiz object. Only the fields grade, sumgrades, decimalpoints and questiondecimalpoints are used.
* @param mixed $round false = don't round, true = round using quiz_format_grade, 'question' = round using quiz_format_question_grade.
* @return float the rescaled grade.
*/
function quiz_rescale_grade($rawgrade, $quiz, $round = true) {
if ($quiz->sumgrades != 0) {
$grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
if ($round === 'question') { // === really necessary here true == 'question' is true in PHP!
$grade = quiz_format_question_grade($quiz, $grade);
} else if ($round) {
$grade = quiz_format_grade($quiz, $grade);
}
} else {
$grade = 0;
}
return $grade;
}
/**
* Get the feedback text that should be show to a student who
* got this grade on this quiz. The feedback is processed ready for diplay.
*
* @param float $grade a grade on this quiz.
* @param integer $quizid the id of the quiz object.
* @return string the comment that corresponds to this grade (empty string if there is not one.
*/
function quiz_feedback_for_grade($grade, $quiz, $context, $cm=null) {
global $DB;
$feedback = $DB->get_record_select('quiz_feedback', "quizid = ? AND mingrade <= ? AND $grade < maxgrade", array($quiz->id, $grade));
if (empty($feedback->feedbacktext)) {
$feedback->feedbacktext = '';
}
// Clean the text, ready for display.
$formatoptions = new stdClass;
$formatoptions->noclean = true;
$feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', $context->id, 'mod_quiz', 'feedback', $feedback->id);
$feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
return $feedbacktext;
}
/**
* @param object $quiz the quiz database row.
* @return boolean Whether this quiz has any non-blank feedback text.
*/
function quiz_has_feedback($quiz) {
global $DB;
static $cache = array();
if (!array_key_exists($quiz->id, $cache)) {
$cache[$quiz->id] = quiz_has_grades($quiz) &&
$DB->record_exists_select('quiz_feedback', "quizid = ? AND " .
$DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true),
array($quiz->id));
}
return $cache[$quiz->id];
}
/**
* The quiz grade is the score that student's results are marked out of. When it
* changes, the corresponding data in quiz_grades and quiz_feedback needs to be
* rescaled.
*
* @param float $newgrade the new maximum grade for the quiz.
* @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
* @return boolean indicating success or failure. TODO: MDL-20625
*/
function quiz_set_grade($newgrade, &$quiz) {
global $DB;
// This is potentially expensive, so only do it if necessary.
if (abs($quiz->grade - $newgrade) < 1e-7) {
// Nothing to do.
return true;
}
// Use a transaction, so that on those databases that support it, this is safer.
$transaction = $DB->start_delegated_transaction();
try {
// Update the quiz table.
$DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
// Rescaling the other data is only possible if the old grade was non-zero.
if ($quiz->grade > 1e-7) {
global $CFG;
$factor = $newgrade/$quiz->grade;
$quiz->grade = $newgrade;
// Update the quiz_grades table.
$timemodified = time();
$DB->execute("
UPDATE {quiz_grades}
SET grade = ? * grade, timemodified = ?
WHERE quiz = ?
", array($factor, $timemodified, $quiz->id));
// Update the quiz_feedback table.
$DB->execute("
UPDATE {quiz_feedback}
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
WHERE quizid = ?
", array($factor, $factor, $quiz->id));
}
// update grade item and send all grades to gradebook
quiz_grade_item_update($quiz);
quiz_update_grades($quiz);
$transaction->allow_commit();
return true;
} catch (Exception $e) {
//TODO: MDL-20625 this part was returning false, but now throws exception
$transaction->rollback($e);
}
}
/**
* Save the overall grade for a user at a quiz in the quiz_grades table
*
* @param object $quiz The quiz for which the best grade is to be calculated and then saved.
* @param integer $userid The userid to calculate the grade for. Defaults to the current user.
* @param array $attempts The attempts of this user. Useful if you are
* looping through many users. Attempts can be fetched in one master query to
* avoid repeated querying.
* @return boolean Indicates success or failure.
*/
function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
global $DB;
global $USER, $OUTPUT;
if (empty($userid)) {
$userid = $USER->id;
}
if (!$attempts){
// Get all the attempts made by the user
if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
echo $OUTPUT->notification('Could not find any user attempts');
return false;
}
}
// Calculate the best grade
$bestgrade = quiz_calculate_best_grade($quiz, $attempts);
$bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
// Save the best grade in the database
if ($grade = $DB->get_record('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid))) {
$grade->grade = $bestgrade;
$grade->timemodified = time();
$DB->update_record('quiz_grades', $grade);
} else {
$grade->quiz = $quiz->id;
$grade->userid = $userid;
$grade->grade = $bestgrade;
$grade->timemodified = time();
$DB->insert_record('quiz_grades', $grade);
}
quiz_update_grades($quiz, $userid);
return true;
}
/**
* Calculate the overall grade for a quiz given a number of attempts by a particular user.
*
* @return float The overall grade
* @param object $quiz The quiz for which the best grade is to be calculated
* @param array $attempts An array of all the attempts of the user at the quiz
*/
function quiz_calculate_best_grade($quiz, $attempts) {
switch ($quiz->grademethod) {
case QUIZ_ATTEMPTFIRST:
foreach ($attempts as $attempt) {
return $attempt->sumgrades;
}
break;
case QUIZ_ATTEMPTLAST:
foreach ($attempts as $attempt) {
$final = $attempt->sumgrades;
}
return $final;
case QUIZ_GRADEAVERAGE:
$sum = 0;
$count = 0;
foreach ($attempts as $attempt) {
$sum += $attempt->sumgrades;
$count++;
}
return (float)$sum/$count;
default:
case QUIZ_GRADEHIGHEST:
$max = 0;
foreach ($attempts as $attempt) {
if ($attempt->sumgrades > $max) {
$max = $attempt->sumgrades;
}
}
return $max;
}
}
/**
* Return the attempt with the best grade for a quiz
*
* Which attempt is the best depends on $quiz->grademethod. If the grade
* method is GRADEAVERAGE then this function simply returns the last attempt.
* @return object The attempt with the best grade
* @param object $quiz The quiz for which the best grade is to be calculated
* @param array $attempts An array of all the attempts of the user at the quiz
*/
function quiz_calculate_best_attempt($quiz, $attempts) {
switch ($quiz->grademethod) {
case QUIZ_ATTEMPTFIRST:
foreach ($attempts as $attempt) {
return $attempt;
}
break;
case QUIZ_GRADEAVERAGE: // need to do something with it :-)
case QUIZ_ATTEMPTLAST:
foreach ($attempts as $attempt) {
$final = $attempt;
}
return $final;
default:
case QUIZ_GRADEHIGHEST:
$max = -1;
foreach ($attempts as $attempt) {
if ($attempt->sumgrades > $max) {
$max = $attempt->sumgrades;
$maxattempt = $attempt;
}
}
return $maxattempt;
}
}
/**
* @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
* @return the lang string for that option.
*/
function quiz_get_grading_option_name($option) {
$strings = quiz_get_grading_options();
return $strings[$option];
}
/// Other quiz functions ////////////////////////////////////////////////////
/**
* Parse field names used for the replace options on question edit forms
*/
function quiz_parse_fieldname($name, $nameprefix='question') {
$reg = array();
if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
return array('mode' => $reg[2], 'id' => (int)$reg[1]);
} else {
return false;
}
}
/**
* Upgrade states for an attempt to Moodle 1.5 model
*
* Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
* The reason these are still around is that for large sites it would have taken too long to
* upgrade all states at once. This function sets the timestamp field and creates an entry in the
* question_sessions table.
* @param object $attempt The attempt whose states need upgrading
*/
function quiz_upgrade_states($attempt) {
global $DB;
global $CFG;
// The old quiz model only allowed a single response per quiz attempt so that there will be
// only one state record per question for this attempt.
// We set the timestamp of all states to the timemodified field of the attempt.
$DB->execute("UPDATE {question_states} SET timestamp = ? WHERE attempt = ?", array($attempt->timemodified, $attempt->uniqueid));
// For each state we create an entry in the question_sessions table, with both newest and
// newgraded pointing to this state.
// Actually we only do this for states whose question is actually listed in $attempt->layout.
// We do not do it for states associated to wrapped questions like for example the questions
// used by a RANDOM question
$session = new stdClass;
$session->attemptid = $attempt->uniqueid;
$questionlist = quiz_questions_in_quiz($attempt->layout);
$params = array($attempt->uniqueid);
list($usql, $question_params) = $DB->get_in_or_equal(explode(',',$questionlist));
$params = array_merge($params, $question_params);
if ($questionlist and $states = $DB->get_records_select('question_states', "attempt = ? AND question $usql", $params)) {
foreach ($states as $state) {
$session->newgraded = $state->id;
$session->newest = $state->id;
$session->questionid = $state->question;
$DB->insert_record('question_sessions', $session, false);
}
}
}
/**
* @param object $quiz the quiz.
* @param integer $cmid the course_module object for this quiz.
* @param object $question the question.
* @param string $returnurl url to return to after action is done.
* @return string html for a number of icons linked to action pages for a
* question - preview and edit / view icons depending on user capabilities.
*/
function quiz_question_action_icons($quiz, $cmid, $question, $returnurl) {
$html = quiz_question_preview_button($quiz, $question) . ' ' .
quiz_question_edit_button($cmid, $question, $returnurl);
return $html;
}
/**
* @param integer $cmid the course_module.id for this quiz.
* @param object $question the question.
* @param string $returnurl url to return to after action is done.
* @param string $contentbeforeicon some HTML content to be added inside the link, before the icon.
* @return the HTML for an edit icon, view icon, or nothing for a question (depending on permissions).
*/
function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') {
global $CFG, $OUTPUT;
// Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
static $stredit = null;
static $strview = null;
if ($stredit === null){
$stredit = get_string('edit');
$strview = get_string('view');
}
// What sort of icon should we show?
$action = '';
if (question_has_capability_on($question, 'edit', $question->category) ||
question_has_capability_on($question, 'move', $question->category)) {
$action = $stredit;
$icon = '/t/edit';
} else if (question_has_capability_on($question, 'view', $question->category)) {
$action = $strview;
$icon = '/i/info';
}
// Build the icon.
if ($action) {
$questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id);
$questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams);
return '<a title="' . $action . '" href="' . $questionurl->out() . '"><img src="' .
$OUTPUT->pix_url($icon) . '" alt="' . $action . '" />' . $contentaftericon .
'</a>';
} else {
return $contentaftericon;
}
}
/**
* @param object $quiz the quiz
* @param object $question the question
* @param boolean $label if true, show the previewquestion label after the icon
* @return the HTML for a preview question icon.
*/
function quiz_question_preview_button($quiz, $question, $label = false) {
global $CFG, $COURSE, $OUTPUT;
if (!question_has_capability_on($question, 'use', $question->category)) {
return '';
}
// Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
static $strpreview = null;
static $strpreviewquestion = null;
if ($strpreview === null){
$strpreview = get_string('preview', 'quiz');
$strpreviewquestion = get_string('previewquestion', 'quiz');
}
// Do we want a label?
$strpreviewlabel="";
if ($label) {
$strpreviewlabel = $strpreview;
}
// Build the icon.
$image = $OUTPUT->pix_icon('t/preview', $strpreviewquestion);
$link = new moodle_url($CFG->wwwroot."/question/preview.php?id=$question->id&quizid=$quiz->id");
parse_str(QUESTION_PREVIEW_POPUP_OPTIONS, $options);
$action = new popup_action('click', $link, 'questionpreview', $options);
return $OUTPUT->action_link($link, $image, $action, array('title' => $strpreviewquestion));
}
/**
* @param object $attempt the attempt.
* @param object $context the quiz context.
* @return integer whether flags should be shown/editable to the current user for this attempt.
*/
function quiz_get_flag_option($attempt, $context) {
global $USER;
static $flagmode = null;
if (is_null($flagmode)) {
if (!has_capability('moodle/question:flag', $context)) {
$flagmode = QUESTION_FLAGSHIDDEN;
} else if ($attempt->userid == $USER->id) {
$flagmode = QUESTION_FLAGSEDITABLE;
} else {
$flagmode = QUESTION_FLAGSSHOWN;
}
}
return $flagmode;
}
/**
* Determine render options
*
* @param int $reviewoptions
* @param object $state
*/
function quiz_get_renderoptions($quiz, $attempt, $context, $state) {
$reviewoptions = $quiz->review;
$options = new stdClass;
$options->flags = quiz_get_flag_option($attempt, $context);
// Show the question in readonly (review) mode if the question is in
// the closed state
$options->readonly = question_state_is_closed($state);
// Show feedback once the question has been graded (if allowed by the quiz)
$options->feedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
// Show correct responses in readonly mode if the quiz allows it
$options->correct_responses = $options->readonly && ($reviewoptions & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
// Show general feedback if the question has been graded and the quiz allows it.
$options->generalfeedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
// Show overallfeedback once the attempt is over.
$options->overallfeedback = false;
// Always show responses and scores
$options->responses = true;
$options->scores = true;
$options->quizstate = QUIZ_STATE_DURING;
$options->history = false;
return $options;
}
/**
* Determine review options
*
* @param object $quiz the quiz instance.
* @param object $attempt the attempt in question.
* @param $context the quiz module context.
*
* @return object an object with boolean fields responses, scores, feedback,
* correct_responses, solutions and general feedback
*/
function quiz_get_reviewoptions($quiz, $attempt, $context) {
global $USER;
$options = new stdClass;
$options->readonly = true;
$options->flags = quiz_get_flag_option($attempt, $context);
// Provide the links to the question review and comment script
if (!empty($attempt->id)) {
$options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id;
}
// Show a link to the comment box only for closed attempts
if ($attempt->timefinish && has_capability('mod/quiz:grade', $context)) {
$options->questioncommentlink = '/mod/quiz/comment.php';
}
// Whether to display a response history.
$canviewreports = has_capability('mod/quiz:viewreports', $context);
$options->history = ($canviewreports && !$attempt->preview) ? 'all' : 'graded';
if ($canviewreports && has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) {
// People who can see reports and hidden grades should be shown everything,
// except during preview when teachers want to see what students see.
$options->responses = true;
$options->scores = true;
$options->feedback = true;
$options->correct_responses = true;
$options->solutions = false;
$options->generalfeedback = true;
$options->overallfeedback = true;
$options->quizstate = QUIZ_STATE_TEACHERACCESS;
} else {
// Work out the state of the attempt ...
if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
$quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
$options->quizstate = QUIZ_STATE_IMMEDIATELY;
} else if (!$quiz->timeclose or time() < $quiz->timeclose) {
$quiz_state_mask = QUIZ_REVIEW_OPEN;
$options->quizstate = QUIZ_STATE_OPEN;
} else {
$quiz_state_mask = QUIZ_REVIEW_CLOSED;
$options->quizstate = QUIZ_STATE_CLOSED;
}
// ... and hence extract the appropriate review options.
$options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
$options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0;
$options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
$options->correct_responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
$options->solutions = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
$options->generalfeedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK) ? 1 : 0;
$options->overallfeedback = $attempt->timefinish && ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_OVERALLFEEDBACK);
}
return $options;
}
/**
* Combines the review options from a number of different quiz attempts.
* Returns an array of two ojects, so he suggested way of calling this
* funciton is:
* list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
*
* @param object $quiz the quiz instance.
* @param array $attempts an array of attempt objects.
* @param $context the roles and permissions context,
* normally the context for the quiz module instance.
*
* @return array of two options objects, one showing which options are true for
* at least one of the attempts, the other showing which options are true
* for all attempts.
*/
function quiz_get_combined_reviewoptions($quiz, $attempts, $context) {
$fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
$someoptions = new stdClass;
$alloptions = new stdClass;
foreach ($fields as $field) {
$someoptions->$field = false;
$alloptions->$field = true;
}
foreach ($attempts as $attempt) {
$attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
foreach ($fields as $field) {
$someoptions->$field = $someoptions->$field || $attemptoptions->$field;
$alloptions->$field = $alloptions->$field && $attemptoptions->$field;
}
}
return array($someoptions, $alloptions);
}
/// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
/**
* Sends confirmation email to the student taking the course
*
* @param stdClass $a associative array of replaceable fields for the templates
*
* @return bool|string result of events_triger
*/
function quiz_send_confirmation($a) {
global $USER;
// recipient is self
$a->useridnumber = $USER->idnumber;
$a->username = fullname($USER);
$a->userusername = $USER->username;
// fetch the subject and body from strings
$subject = get_string('emailconfirmsubject', 'quiz', $a);
$body = get_string('emailconfirmbody', 'quiz', $a);
// send email and analyse result
$eventdata = new object();
$eventdata->modulename = 'quiz';
$eventdata->userfrom = get_admin();
$eventdata->userto = $USER;
$eventdata->subject = $subject;
$eventdata->fullmessage = $body;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = '';
$eventdata->smallmessage = '';
return message_send($eventdata);
}
/**
* Sends notification email to the interested parties that assign the role capability
*
* @param object $recipient user object of the intended recipient
* @param stdClass $a associative array of replaceable fields for the templates
*
* @return bool|string result of events_triger()
*/
function quiz_send_notification($recipient, $a) {
global $USER;
// recipient info for template
$a->username = fullname($recipient);
$a->userusername = $recipient->username;
$a->userusername = $recipient->username;
// fetch the subject and body from strings
$subject = get_string('emailnotifysubject', 'quiz', $a);
$body = get_string('emailnotifybody', 'quiz', $a);
// send email and analyse result
$eventdata = new object();
$eventdata->modulename = 'quiz';
$eventdata->userfrom = $USER;
$eventdata->userto = $recipient;
$eventdata->subject = $subject;
$eventdata->fullmessage = $body;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = '';
$eventdata->smallmessage = '';
return message_send($eventdata);
}
/**
* Takes a bunch of information to format into an email and send
* to the specified recipient.
*
* @param object $course the course
* @param object $quiz the quiz
* @param object $attempt this attempt just finished
* @param object $context the quiz context
* @param object $cm the coursemodule for this quiz
*
* @return int number of emails sent
*/
function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm) {
global $CFG, $USER;
// we will count goods and bads for error logging
$emailresult = array('good' => 0, 'block' => 0, 'fail' => 0);
// do nothing if required objects not present
if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
debugging('quiz_send_notification_emails: Email(s) not sent due to program error.',
DEBUG_DEVELOPER);
return $emailresult['fail'];
}
// check for confirmation required
$sendconfirm = false;
$notifyexcludeusers = '';
if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
// exclude from notify emails later
$notifyexcludeusers = $USER->id;
// send the email
$sendconfirm = true;
}
// check for notifications required
$notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay';
$groups = groups_get_all_groups($course->id, $USER->id);
if (is_array($groups) && count($groups) > 0) {
$groups = array_keys($groups);
} else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) {
// If the user is not in a group, and the quiz is set to group mode,
// then set $gropus to a non-existant id so that only users with
// 'moodle/site:accessallgroups' get notified.
$groups = -1;
} else {
$groups = '';
}
$userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
$notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true);
// if something to send, then build $a
if (! empty($userstonotify) or $sendconfirm) {
$a = new stdClass;
// course info
$a->coursename = $course->fullname;
$a->courseshortname = $course->shortname;
// quiz info
$a->quizname = $quiz->name;
$a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id;
$a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . format_string($quiz->name) . ' report</a>';
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
$a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . format_string($quiz->name) . ' review</a>';
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
$a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
// attempt info
$a->submissiontime = userdate($attempt->timefinish);
$a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
// student who sat the quiz info
$a->studentidnumber = $USER->idnumber;
$a->studentname = fullname($USER);
$a->studentusername = $USER->username;
}
// send confirmation if required
if ($sendconfirm) {
// send the email and update stats
switch (quiz_send_confirmation($a)) {
case true:
$emailresult['good']++;
break;
case false:
$emailresult['fail']++;
break;
case 'emailstop':
$emailresult['block']++;
break;
}
}
// send notifications if required
if (!empty($userstonotify)) {
// loop through recipients and send an email to each and update stats
foreach ($userstonotify as $recipient) {
switch (quiz_send_notification($recipient, $a)) {
case true:
$emailresult['good']++;
break;
case false:
$emailresult['fail']++;
break;
case 'emailstop':
$emailresult['block']++;
break;
}
}
}
// log errors sending emails if any
if (! empty($emailresult['fail'])) {
debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER);
}
if (! empty($emailresult['block'])) {
debugging('quiz_send_notification_emails:: '.$emailresult['block'].' email(s) were blocked by the user.', DEBUG_DEVELOPER);
}
// return the number of successfully sent emails
return $emailresult['good'];
}
/**
* Clean the question layout from various possible anomalies:
* - Remove consecutive ","'s
* - Remove duplicate question id's
* - Remove extra "," from beginning and end
* - Finally, add a ",0" in the end if there is none
*
* @param $string $layout the quiz layout to clean up, usually from $quiz->questions.
* @param boolean $removeemptypages If true, remove empty pages from the quiz. False by default.
* @return $string the cleaned-up layout
*/
function quiz_clean_layout($layout, $removeemptypages = false){
// Remove duplicate "," (or triple, or...)
$layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
// Remove duplicate question ids
$layout = explode(',', $layout);
$cleanerlayout = array();
$seen = array();
foreach ($layout as $item) {
if ($item == 0) {
$cleanerlayout[] = '0';
} else if (!in_array($item, $seen)) {
$cleanerlayout[] = $item;
$seen[] = $item;
}
}
if ($removeemptypages) {
// Avoid duplicate page breaks
$layout = $cleanerlayout;
$cleanerlayout = array();
$stripfollowingbreaks = true; // Ensure breaks are stripped from the start.
foreach ($layout as $item) {
if ($stripfollowingbreaks && $item == 0) {
continue;
}
$cleanerlayout[] = $item;
$stripfollowingbreaks = $item == 0;
}
}
// Add a page break at the end if there is none
if (end($cleanerlayout) !== '0') {
$cleanerlayout[] = '0';
}
return implode(',', $cleanerlayout);
}
/**
* Print a quiz error message. This is a thin wrapper around print_error, for convinience.
*
* @param mixed $quiz either the quiz object, or the interger quiz id.
* @param string $errorcode the name of the string from quiz.php to print.
* @param object $a any extra data required by the error string.
*/
function quiz_error($quiz, $errorcode, $a = null) {
global $CFG;
if (is_object($quiz)) {
$quiz = $quiz->id;
}
print_error($errorcode, 'quiz', $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz, $a);
}
/**
* Checks if browser is safe browser
*
* @return true, if browser is safe browser else false
*/
function quiz_check_safe_browser() {
return strpos($_SERVER['HTTP_USER_AGENT'], "SEB") !== false;
}
function quiz_get_js_module() {
return array(
'name' => 'mod_quiz',
'fullpath' => '/mod/quiz/module.js',
'requires' => array('base', 'dom', 'event-delegate', 'event-key'),
'strings' => array(
array('timesup', 'quiz'),
array('functiondisabledbysecuremode', 'quiz'),
array('flagged', 'question'),
),
);
}