moodle/mod/quiz/locallib.php

958 lines
34 KiB
PHP

<?php // $Id$
/**
* 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.
* @version $Id$
* @author Martin Dougiamas and many others. This has recently been completely
* rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
* the Serving Mathematics project
* {@link http://maths.york.ac.uk/serving_maths}
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package quiz
*/
/**
* 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 . '/question/editlib.php');
/// Constants ///////////////////////////////////////////////////////////////////
/**#@+
* Options determining how the grades from individual attempts are combined to give
* the overall grade for a user
*/
define("QUIZ_GRADEHIGHEST", "1");
define("QUIZ_GRADEAVERAGE", "2");
define("QUIZ_ATTEMPTFIRST", "3");
define("QUIZ_ATTEMPTLAST", "4");
/**#@-*/
/// 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.
* @return object The newly created attempt object.
* @param object $quiz The quiz to create an attempt for.
* @param integer $attemptnumber The sequence number for the attempt.
*/
function quiz_create_attempt($quiz, $attemptnumber) {
global $USER, $CFG;
if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) {
// we are not building on last attempt so create a new attempt
$attempt->quiz = $quiz->id;
$attempt->userid = $USER->id;
$attempt->preview = 0;
if ($quiz->shufflequestions) {
$attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true);
} else {
$attempt->layout = $quiz->questions;
}
}
$timenow = time();
$attempt->attempt = $attemptnumber;
$attempt->sumgrades = 0.0;
$attempt->timestart = $timenow;
$attempt->timefinish = 0;
$attempt->timemodified = $timenow;
$attempt->uniqueid = question_new_attempt_uniqueid();
return $attempt;
}
/**
* Returns an unfinished attempt (if there is one) for the given
* user on the given quiz. This function does not return preview attempts.
*
* @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;
}
}
/**
* @param integer $quizid the quiz id.
* @param integer $userid the userid.
* @param string $status 'all', 'finished' or 'unfinished' to control
* @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
*/
function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
$status_condition = array(
'all' => '',
'finished' => ' AND timefinish > 0',
'unfinished' => ' AND timefinish = 0'
);
$previewclause = '';
if (!$includepreviews) {
$previewclause = ' AND preview = 0';
}
if ($attempts = get_records_select('quiz_attempts',
"quiz = '$quizid' AND userid = '$userid'" . $previewclause . $status_condition[$status],
'attempt ASC')) {
return $attempts;
} else {
return array();
}
}
/**
* Delete a quiz attempt.
*/
function quiz_delete_attempt($attempt, $quiz) {
if (is_numeric($attempt)) {
if (!$attempt = get_record('quiz_attempts', '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;
}
delete_records('quiz_attempts', '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 (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id)) {
delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id);
} else {
quiz_save_best_grade($quiz, $userid);
}
quiz_update_grades($quiz, $userid);
}
/// Functions to do with quiz layout and pages ////////////////////////////////
/**
* Returns a comma separated list of question ids for the current page
*
* @return string Comma separated list of question ids
* @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.
*/
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
*
* @return string Comma separated list of question ids
* @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
*/
function quiz_questions_in_quiz($layout) {
return str_replace(',0', '', $layout);
}
/**
* Returns the number of pages in the quiz layout
*
* @return integer Comma separated list of question ids
* @param string $layout The string representing the quiz layout.
*/
function quiz_number_of_pages($layout) {
return substr_count($layout, ',0');
}
/**
* Returns the first question number for the current quiz page
*
* @return integer The number of the first question
* @param string $quizlayout The string representing the layout for the whole quiz
* @param string $pagelayout The string representing the layout for the current page
*/
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;
$start = strpos($quizlayout, ','.$pagelayout.',')-2;
if ($start > 0) {
$prevlist = substr($quizlayout, 0, $start);
return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}question
WHERE id IN ($prevlist)");
} else {
return 1;
}
}
/**
* Re-paginates the quiz layout
*
* @return string The new layout string
* @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?
*/
function quiz_repaginate($layout, $perpage, $shuffle=false) {
$layout = str_replace(',0', '', $layout); // remove existing page breaks
$questions = explode(',', $layout);
if ($shuffle) {
srand((float)microtime() * 1000000); // for php < 4.2
shuffle($questions);
}
$i = 1;
$layout = '';
foreach ($questions as $question) {
if ($perpage and $i > $perpage) {
$layout .= '0,';
$i = 1;
}
$layout .= $question.',';
$i++;
}
return $layout.'0';
}
/**
* Print navigation panel for quiz attempt and review pages
*
* @param integer $page The number of the current page (counting from 0).
* @param integer $pages The total number of pages.
*/
function quiz_print_navigation_panel($page, $pages) {
//$page++;
echo '<div class="pagingbar">';
echo '<span class="title">' . get_string('page') . ':</span>';
if ($page > 0) {
// Print previous link
$strprev = get_string('previous');
echo '<a href="javascript:navigate(' . ($page - 1) . ');" title="'
. $strprev . '">(' . $strprev . ')</a>';
}
for ($i = 0; $i < $pages; $i++) {
if ($i == $page) {
echo '<span class="thispage">'.($i+1).'</span>';
} else {
echo '<a href="javascript:navigate(' . ($i) . ');">'.($i+1).'</a>';
}
}
if ($page < $pages - 1) {
// Print next link
$strnext = get_string('next');
echo '<a href="javascript:navigate(' . ($page + 1) . ');" title="'
. $strnext . '">(' . $strnext . ')</a>';
}
echo '</div>';
}
/// 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.
* @return array Array of grades indexed by question id
* These are the maximum possible grades that
* students can achieve for each of the questions
* @param integer $quiz The quiz object
*/
function quiz_get_all_question_grades($quiz) {
global $CFG;
$questionlist = quiz_questions_in_quiz($quiz->questions);
if (empty($questionlist)) {
return array();
}
$instances = get_records_sql("SELECT question,grade,id
FROM {$CFG->prefix}quiz_question_instances
WHERE quiz = '$quiz->id'" .
(is_null($questionlist) ? '' :
"AND question IN ($questionlist)"));
$list = explode(",", $questionlist);
$grades = array();
foreach ($list as $qid) {
if (isset($instances[$qid])) {
$grades[$qid] = $instances[$qid]->grade;
} else {
$grades[$qid] = 1;
}
}
return $grades;
}
/**
* Get the best current grade for a particular user in a quiz.
*
* @param object $quiz the quiz object.
* @param integer $userid the id of the user.
* @return float the user's current grade for this quiz.
*/
function quiz_get_best_grade($quiz, $userid) {
$grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id, 'userid', $userid);
// Need to detect errors/no result, without catching 0 scores.
if (is_numeric($grade)) {
return round($grade,$quiz->decimalpoints);
} else {
return NULL;
}
}
/**
* 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 and decimalpoints are used.
* @return float the rescaled grade.
*/
function quiz_rescale_grade($rawgrade, $quiz) {
if ($quiz->sumgrades) {
return round($rawgrade*$quiz->grade/$quiz->sumgrades, $quiz->decimalpoints);
} else {
return 0;
}
}
/**
* 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, $quizid) {
$feedback = get_field_select('quiz_feedback', 'feedbacktext',
"quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade");
if (empty($feedback)) {
$feedback = '';
}
// Clean the text, ready for display.
$formatoptions = new stdClass;
$formatoptions->noclean = true;
$feedback = format_text($feedback, FORMAT_MOODLE, $formatoptions);
return $feedback;
}
/**
* @param integer $quizid the id of the quiz object.
* @return boolean Whether this quiz has any non-blank feedback text.
*/
function quiz_has_feedback($quizid) {
static $cache = array();
if (!array_key_exists($quizid, $cache)) {
$cache[$quizid] = record_exists_select('quiz_feedback',
"quizid = $quizid AND feedbacktext <> ''");
}
return $cache[$quizid];
}
/**
* 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.
*/
function quiz_set_grade($newgrade, &$quiz) {
// 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.
begin_sql();
// Update the quiz table.
$success = set_field('quiz', 'grade', $newgrade, '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();
$success = $success && execute_sql("
UPDATE {$CFG->prefix}quiz_grades
SET grade = $factor * grade, timemodified = $timemodified
WHERE quiz = $quiz->id
", false);
// Update the quiz_grades table.
$success = $success && execute_sql("
UPDATE {$CFG->prefix}quiz_feedback
SET mingrade = $factor * mingrade, maxgrade = $factor * maxgrade
WHERE quizid = $quiz->id
", false);
}
// update grade item and send all grades to gradebook
quiz_grade_item_update($quiz);
quiz_update_grades($quiz);
if ($success) {
return commit_sql();
} else {
rollback_sql();
return false;
}
}
/**
* 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.
* @return boolean Indicates success or failure.
*/
function quiz_save_best_grade($quiz, $userid = null) {
global $USER;
if (empty($userid)) {
$userid = $USER->id;
}
// Get all the attempts made by the user
if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
notify('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);
// Save the best grade in the database
if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) {
$grade->grade = $bestgrade;
$grade->timemodified = time();
if (!update_record('quiz_grades', $grade)) {
notify('Could not update best grade');
return false;
}
} else {
$grade->quiz = $quiz->id;
$grade->userid = $userid;
$grade->grade = $bestgrade;
$grade->timemodified = time();
if (!insert_record('quiz_grades', $grade)) {
notify('Could not insert new best grade');
return false;
}
}
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;
}
}
/**
* @return the options for calculating the quiz grade from the individual attempt grades.
*/
function quiz_get_grading_options() {
return array (
QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
}
/**
* @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 ////////////////////////////////////////////////////
/**
* Print a box with quiz start and due dates
*
* @param object $quiz
*/
function quiz_view_dates($quiz) {
if (!$quiz->timeopen && !$quiz->timeclose) {
return;
}
print_simple_box_start('center', '', '', '', 'generalbox', 'dates');
echo '<table>';
if ($quiz->timeopen) {
echo '<tr><td class="c0">'.get_string("quizopen", "quiz").':</td>';
echo ' <td class="c1">'.userdate($quiz->timeopen).'</td></tr>';
}
if ($quiz->timeclose) {
echo '<tr><td class="c0">'.get_string("quizclose", "quiz").':</td>';
echo ' <td class="c1">'.userdate($quiz->timeclose).'</td></tr>';
}
echo '</table>';
print_simple_box_end();
}
/**
* 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 $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.
execute_sql("UPDATE {$CFG->prefix}question_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->uniqueid'", false);
// 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);
if ($questionlist and $states = get_records_select('question_states', "attempt = '$attempt->uniqueid' AND question IN ($questionlist)")) {
foreach ($states as $state) {
$session->newgraded = $state->id;
$session->newest = $state->id;
$session->questionid = $state->question;
insert_record('question_sessions', $session, false);
}
}
}
/**
* @param object $quiz the quiz
* @param object $question the question
* @return the HTML for a preview question icon.
*/
function quiz_question_preview_button($quiz, $question) {
global $CFG, $COURSE;
if (!question_has_capability_on($question, 'use', $question->category)){
return '';
}
$strpreview = get_string('previewquestion', 'quiz');
$quizorcourseid = $quiz->id?('&amp;quizid=' . $quiz->id):('&amp;courseid=' .$COURSE->id);
return link_to_popup_window('/question/preview.php?id=' . $question->id . $quizorcourseid, 'questionpreview',
"<img src=\"$CFG->pixpath/t/preview.gif\" class=\"iconsmall\" alt=\"$strpreview\" />",
0, 0, $strpreview, QUESTION_PREVIEW_POPUP_OPTIONS, true);
}
/**
* Determine render options
*
* @param int $reviewoptions
* @param object $state
*/
function quiz_get_renderoptions($reviewoptions, $state) {
$options = new stdClass;
// 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 validation only after a validation event
$options->validation = QUESTION_EVENTVALIDATE === $state->event;
// 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;
return $options;
}
/**
* Determine review options
*
* @param object $quiz the quiz instance.
* @param object $attempt the attempt in question.
* @param $context the roles and permissions context,
* normally the context for the quiz module instance.
*
* @return object an object with boolean fields responses, scores, feedback,
* correct_responses, solutions and general feedback
*/
function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
$options = new stdClass;
$options->readonly = true;
// Provide the links to the question review and comment script
$options->questionreviewlink = '/mod/quiz/reviewquestion.php';
if ($context && has_capability('mod/quiz:viewreports', $context) and !$attempt->preview) {
// The teacher should be shown everything except during preview when the teachers
// wants to see just what the students see
$options->responses = true;
$options->scores = true;
$options->feedback = true;
$options->correct_responses = true;
$options->solutions = false;
$options->generalfeedback = true;
$options->overallfeedback = true;
// Show a link to the comment box only for closed attempts
if ($attempt->timefinish) {
$options->questioncommentlink = '/mod/quiz/comment.php';
}
} else {
if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
$quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
} else if (!$quiz->timeclose or time() < $quiz->timeclose) {
$quiz_state_mask = QUIZ_REVIEW_OPEN;
} else {
$quiz_state_mask = QUIZ_REVIEW_CLOSED;
}
$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=null) {
$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 email_to_user()
*/
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
return email_to_user($USER, get_admin(), $subject, $body);
}
/**
* 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 email_to_user()
*/
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
return email_to_user($recipient, $USER, $subject, $body);
}
/**
* 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';
$userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
$notifyfields, '', '', '', array_keys(groups_get_all_groups($course->id, $USER->id)),
$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->quizreportlink = '<a href="report.php?q=' . $quiz->id . '">' . format_string($quiz->name) . ' report</a>';
$a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id;
$a->quizreviewlink = '<a href="review.php?attempt=' . $attempt->id . '">' . format_string($quiz->name) . ' review</a>';
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
$a->quizlink = '<a href="view.php?q=' . $quiz->id . '">' . format_string($quiz->name) . '</a>';
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id;
// 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'];
}
?>