2009-11-04 11:58:30 +00:00
|
|
|
<?php
|
2011-02-03 18:06:10 +00:00
|
|
|
// This file is part of Moodle - http://moodle.org/
|
|
|
|
//
|
|
|
|
// Moodle 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 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// Moodle 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.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* 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.
|
2007-11-07 15:26:37 +00:00
|
|
|
*
|
2011-02-21 16:13:25 +00:00
|
|
|
* @package mod
|
2011-02-03 18:06:10 +00:00
|
|
|
* @subpackage quiz
|
2011-02-21 16:13:25 +00:00
|
|
|
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
|
|
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2008-07-08 18:22:18 +00:00
|
|
|
|
2011-02-23 15:03:35 +00:00
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
|
2007-04-10 14:37:36 +00:00
|
|
|
require_once($CFG->dirroot . '/mod/quiz/lib.php');
|
2011-10-03 18:29:47 +01:00
|
|
|
require_once($CFG->dirroot . '/mod/quiz/accessmanager.php');
|
2011-10-07 17:43:15 +01:00
|
|
|
require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php');
|
2011-05-12 21:03:24 +01:00
|
|
|
require_once($CFG->dirroot . '/mod/quiz/renderer.php');
|
2008-07-08 16:33:47 +00:00
|
|
|
require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
|
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
|
|
|
require_once($CFG->dirroot . '/question/editlib.php');
|
2008-07-24 08:38:03 +00:00
|
|
|
require_once($CFG->libdir . '/eventslib.php');
|
2011-01-23 18:34:41 +01:00
|
|
|
require_once($CFG->libdir . '/filelib.php');
|
2005-01-08 20:06:00 +00:00
|
|
|
|
|
|
|
|
2008-03-07 12:33:07 +00:00
|
|
|
/**
|
2012-05-04 12:40:21 +01:00
|
|
|
* @var int We show the countdown timer if there is less than this amount of time left before the
|
2008-03-07 12:33:07 +00:00
|
|
|
* the quiz close date. (1 hour)
|
|
|
|
*/
|
|
|
|
define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600');
|
|
|
|
|
MDL-3030 quiz overdue handling: trigger automatic state transitions.
Here, we catch all the places where a student might be accessing their
own attempts, and make sure any automatic state transitions that
should happen, do happen, before the student sees the attempt.
The places where we need to check this are view.php, startattempt.php
and processattempt.php.
We do not really need to check attempt.php or summary.php, because if
the student is on one of those pages, the JavaScript timer will
auto-submit when time expires, taking them to processattempt.php,
which will do the acutal work.
We intentionally do not trigger state transition when a teacher is
looking at a student's quiz attemp. We will trigger state transitions
on cron, but that is still to do.
Also, the body of the process_... methods still needs to be written.
2012-04-18 19:18:55 +01:00
|
|
|
/**
|
|
|
|
* @var int If there are fewer than this many seconds left when the student submits
|
|
|
|
* a page of the quiz, then do not take them to the next page of the quiz. Instead
|
|
|
|
* close the quiz immediately.
|
|
|
|
*/
|
|
|
|
define('QUIZ_MIN_TIME_TO_CONTINUE', '2');
|
|
|
|
|
2013-05-09 19:55:26 +05:30
|
|
|
/**
|
|
|
|
* @var int We show no image when user selects No image from dropdown menu in quiz settings.
|
|
|
|
*/
|
|
|
|
define('QUIZ_SHOWIMAGE_NONE', 0);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int We show small image when user selects small image from dropdown menu in quiz settings.
|
|
|
|
*/
|
|
|
|
define('QUIZ_SHOWIMAGE_SMALL', 1);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int We show Large image when user selects Large image from dropdown menu in quiz settings.
|
|
|
|
*/
|
|
|
|
define('QUIZ_SHOWIMAGE_LARGE', 2);
|
|
|
|
|
MDL-3030 quiz overdue handling: trigger automatic state transitions.
Here, we catch all the places where a student might be accessing their
own attempts, and make sure any automatic state transitions that
should happen, do happen, before the student sees the attempt.
The places where we need to check this are view.php, startattempt.php
and processattempt.php.
We do not really need to check attempt.php or summary.php, because if
the student is on one of those pages, the JavaScript timer will
auto-submit when time expires, taking them to processattempt.php,
which will do the acutal work.
We intentionally do not trigger state transition when a teacher is
looking at a student's quiz attemp. We will trigger state transitions
on cron, but that is still to do.
Also, the body of the process_... methods still needs to be written.
2012-04-18 19:18:55 +01:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Functions related to attempts ///////////////////////////////////////////////
|
2005-05-06 06:24:04 +00:00
|
|
|
|
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* 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.
|
2008-03-07 12:33:07 +00:00
|
|
|
*
|
2012-10-09 13:48:30 -04:00
|
|
|
* @param object $quizobj the quiz object to create an attempt for.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $attemptnumber the sequence number for the attempt.
|
2008-03-07 12:33:07 +00:00
|
|
|
* @param object $lastattempt the previous attempt by this user, if any. Only needed
|
|
|
|
* if $attemptnumber > 1 and $quiz->attemptonlast is true.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $timenow the time the attempt was started at.
|
|
|
|
* @param bool $ispreview whether this new attempt is a preview.
|
2013-09-03 12:15:00 +07:00
|
|
|
* @param int $userid the id of the user attempting this quiz.
|
2008-03-07 12:33:07 +00:00
|
|
|
*
|
|
|
|
* @return object the newly created attempt object.
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2013-09-03 12:15:00 +07:00
|
|
|
function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timenow, $ispreview = false, $userid = null) {
|
2008-03-07 12:33:07 +00:00
|
|
|
global $USER;
|
2005-05-06 06:24:04 +00:00
|
|
|
|
2013-09-03 12:15:00 +07:00
|
|
|
if ($userid === null) {
|
|
|
|
$userid = $USER->id;
|
|
|
|
}
|
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
$quiz = $quizobj->get_quiz();
|
2012-01-11 16:21:47 +00:00
|
|
|
if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) {
|
|
|
|
throw new moodle_exception('cannotstartgradesmismatch', 'quiz',
|
2012-03-09 11:28:20 +00:00
|
|
|
new moodle_url('/mod/quiz/view.php', array('q' => $quiz->id)),
|
|
|
|
array('grade' => quiz_format_grade($quiz, $quiz->grade)));
|
2012-01-11 16:21:47 +00:00
|
|
|
}
|
|
|
|
|
2008-04-09 13:01:40 +00:00
|
|
|
if ($attemptnumber == 1 || !$quiz->attemptonlast) {
|
2011-05-12 00:30:25 +01:00
|
|
|
// We are not building on last attempt so create a new attempt.
|
2011-02-21 18:10:19 +00:00
|
|
|
$attempt = new stdClass();
|
2005-05-07 18:50:32 +00:00
|
|
|
$attempt->quiz = $quiz->id;
|
2013-09-03 12:15:00 +07:00
|
|
|
$attempt->userid = $userid;
|
2005-05-07 18:50:32 +00:00
|
|
|
$attempt->preview = 0;
|
2011-07-07 12:37:40 +01:00
|
|
|
$attempt->layout = quiz_clean_layout($quiz->questions, true);
|
2005-05-07 18:50:32 +00:00
|
|
|
if ($quiz->shufflequestions) {
|
2011-07-07 12:37:40 +01:00
|
|
|
$attempt->layout = quiz_repaginate($attempt->layout, $quiz->questionsperpage, true);
|
2005-05-07 18:50:32 +00:00
|
|
|
}
|
2008-03-07 12:33:07 +00:00
|
|
|
} else {
|
2011-05-12 00:30:25 +01:00
|
|
|
// Build on last attempt.
|
2008-03-07 12:33:07 +00:00
|
|
|
if (empty($lastattempt)) {
|
2008-04-04 02:54:20 +00:00
|
|
|
print_error('cannotfindprevattempt', 'quiz');
|
2008-03-07 12:33:07 +00:00
|
|
|
}
|
|
|
|
$attempt = $lastattempt;
|
2005-05-07 18:50:32 +00:00
|
|
|
}
|
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
$attempt->attempt = $attemptnumber;
|
|
|
|
$attempt->timestart = $timenow;
|
|
|
|
$attempt->timefinish = 0;
|
|
|
|
$attempt->timemodified = $timenow;
|
2012-04-24 15:01:12 +01:00
|
|
|
$attempt->state = quiz_attempt::IN_PROGRESS;
|
2005-05-07 18:50:32 +00:00
|
|
|
|
2011-05-12 00:30:25 +01:00
|
|
|
// If this is a preview, mark it as such.
|
2008-03-07 12:33:07 +00:00
|
|
|
if ($ispreview) {
|
|
|
|
$attempt->preview = 1;
|
|
|
|
}
|
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
$timeclose = $quizobj->get_access_manager($timenow)->get_end_time($attempt);
|
|
|
|
if ($timeclose === false || $ispreview) {
|
|
|
|
$attempt->timecheckstate = null;
|
|
|
|
} else {
|
|
|
|
$attempt->timecheckstate = $timeclose;
|
|
|
|
}
|
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
return $attempt;
|
|
|
|
}
|
2013-07-06 13:22:44 +07:00
|
|
|
/**
|
|
|
|
* Start a normal, new, quiz attempt.
|
|
|
|
*
|
2013-07-14 19:26:35 +07:00
|
|
|
* @param quiz $quizobj the quiz object to start an attempt for.
|
|
|
|
* @param question_usage_by_activity $quba
|
|
|
|
* @param object $attempt
|
|
|
|
* @param integer $attemptnumber starting from 1
|
|
|
|
* @param integer $timenow the attempt start time
|
|
|
|
* @param array $questionids slot number => question id. Used for random questions, to force the choice
|
|
|
|
* of a particular actual question. Intended for testing purposes only.
|
|
|
|
* @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants,
|
|
|
|
* to force the choice of a particular variant. Intended for testing
|
|
|
|
* purposes only.
|
2013-07-14 19:08:53 +07:00
|
|
|
* @throws moodle_exception
|
2013-07-14 19:26:35 +07:00
|
|
|
* @return object modified attempt object
|
2013-07-06 13:22:44 +07:00
|
|
|
*/
|
2013-07-14 19:26:35 +07:00
|
|
|
function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow,
|
|
|
|
$questionids = array(), $forcedvariantsbyslot = array()) {
|
2013-07-06 13:22:44 +07:00
|
|
|
// Fully load all the questions in this quiz.
|
|
|
|
$quizobj->preload_questions();
|
|
|
|
$quizobj->load_questions();
|
|
|
|
|
|
|
|
// Add them all to the $quba.
|
|
|
|
$idstoslots = array();
|
|
|
|
$questionsinuse = array_keys($quizobj->get_questions());
|
|
|
|
foreach ($quizobj->get_questions() as $i => $questiondata) {
|
|
|
|
if ($questiondata->qtype != 'random') {
|
|
|
|
if (!$quizobj->get_quiz()->shuffleanswers) {
|
|
|
|
$questiondata->options->shuffleanswers = false;
|
|
|
|
}
|
|
|
|
$question = question_bank::make_question($questiondata);
|
|
|
|
|
|
|
|
} else {
|
2013-07-14 19:08:53 +07:00
|
|
|
if (!isset($questionids[$quba->next_slot_number()])) {
|
|
|
|
$forcequestionid = null;
|
|
|
|
} else {
|
|
|
|
$forcequestionid = $questionids[$quba->next_slot_number()];
|
|
|
|
}
|
|
|
|
|
2013-07-06 13:22:44 +07:00
|
|
|
$question = question_bank::get_qtype('random')->choose_other_question(
|
2013-07-14 19:08:53 +07:00
|
|
|
$questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers, $forcequestionid);
|
2013-07-06 13:22:44 +07:00
|
|
|
if (is_null($question)) {
|
|
|
|
throw new moodle_exception('notenoughrandomquestions', 'quiz',
|
|
|
|
$quizobj->view_url(), $questiondata);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark);
|
|
|
|
$questionsinuse[] = $question->id;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start all the questions.
|
|
|
|
if ($attempt->preview) {
|
|
|
|
$variantoffset = rand(1, 100);
|
|
|
|
} else {
|
|
|
|
$variantoffset = $attemptnumber;
|
|
|
|
}
|
2013-10-29 16:19:24 +00:00
|
|
|
$variantstrategy = new question_variant_pseudorandom_no_repeats_strategy(
|
|
|
|
$variantoffset, $attempt->userid, $quizobj->get_quizid());
|
2013-07-14 19:26:35 +07:00
|
|
|
|
|
|
|
if (!empty($forcedvariantsbyslot)) {
|
|
|
|
$forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array(
|
|
|
|
$forcedvariantsbyslot, $quba);
|
|
|
|
$variantstrategy = new question_variant_forced_choices_selection_strategy(
|
|
|
|
$forcedvariantsbyseed, $variantstrategy);
|
|
|
|
}
|
|
|
|
|
|
|
|
$quba->start_all_questions($variantstrategy, $timenow);
|
2013-07-06 13:22:44 +07:00
|
|
|
|
|
|
|
// Update attempt layout.
|
|
|
|
$newlayout = array();
|
|
|
|
foreach (explode(',', $attempt->layout) as $qid) {
|
|
|
|
if ($qid != 0) {
|
|
|
|
$newlayout[] = $idstoslots[$qid];
|
|
|
|
} else {
|
|
|
|
$newlayout[] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$attempt->layout = implode(',', $newlayout);
|
|
|
|
return $attempt;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start a subsequent new attempt, in each attempt builds on last mode.
|
|
|
|
*
|
|
|
|
* @param question_usage_by_activity $quba this question usage
|
|
|
|
* @param object $attempt this attempt
|
|
|
|
* @param object $lastattempt last attempt
|
|
|
|
* @return object modified attempt object
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) {
|
|
|
|
$oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid);
|
|
|
|
|
|
|
|
$oldnumberstonew = array();
|
|
|
|
foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) {
|
|
|
|
$newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark());
|
|
|
|
|
|
|
|
$quba->start_question_based_on($newslot, $oldqa);
|
|
|
|
|
|
|
|
$oldnumberstonew[$oldslot] = $newslot;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update attempt layout.
|
|
|
|
$newlayout = array();
|
|
|
|
foreach (explode(',', $lastattempt->layout) as $oldslot) {
|
|
|
|
if ($oldslot != 0) {
|
|
|
|
$newlayout[] = $oldnumberstonew[$oldslot];
|
|
|
|
} else {
|
|
|
|
$newlayout[] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$attempt->layout = implode(',', $newlayout);
|
|
|
|
return $attempt;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The save started question usage and quiz attempt in db and log the started attempt.
|
|
|
|
*
|
|
|
|
* @param quiz $quizobj
|
|
|
|
* @param question_usage_by_activity $quba
|
|
|
|
* @param object $attempt
|
|
|
|
* @return object attempt object with uniqueid and id set.
|
|
|
|
*/
|
|
|
|
function quiz_attempt_save_started($quizobj, $quba, $attempt) {
|
|
|
|
global $DB;
|
|
|
|
// Save the attempt in the database.
|
|
|
|
question_engine::save_questions_usage_by_activity($quba);
|
|
|
|
$attempt->uniqueid = $quba->get_id();
|
|
|
|
$attempt->id = $DB->insert_record('quiz_attempts', $attempt);
|
|
|
|
// Log the new attempt.
|
|
|
|
if ($attempt->preview) {
|
|
|
|
add_to_log($quizobj->get_courseid(), 'quiz', 'preview', 'view.php?id='.$quizobj->get_cmid(),
|
|
|
|
$quizobj->get_quizid(), $quizobj->get_cmid());
|
|
|
|
} else {
|
|
|
|
add_to_log($quizobj->get_courseid(), 'quiz', 'attempt', 'review.php?attempt='.$attempt->id,
|
|
|
|
$quizobj->get_quizid(), $quizobj->get_cmid());
|
|
|
|
}
|
|
|
|
return $attempt;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fire an event to tell the rest of Moodle a quiz attempt has started.
|
|
|
|
*
|
|
|
|
* @param object $attempt
|
|
|
|
* @param quiz $quizobj
|
|
|
|
*/
|
|
|
|
function quiz_fire_attempt_started_event($attempt, $quizobj) {
|
|
|
|
// Trigger event.
|
2013-09-03 13:38:44 +08:00
|
|
|
$eventdata = array();
|
|
|
|
$eventdata['context'] = $quizobj->get_context();
|
|
|
|
$eventdata['courseid'] = $quizobj->get_courseid();
|
|
|
|
$eventdata['relateduserid'] = $attempt->userid;
|
|
|
|
$eventdata['objectid'] = $attempt->id;
|
|
|
|
$event = \mod_quiz\event\attempt_started::create($eventdata);
|
|
|
|
$event->add_record_snapshot('quiz', $quizobj->get_quiz());
|
|
|
|
$event->add_record_snapshot('quiz_attempts', $attempt);
|
|
|
|
$event->trigger();
|
2013-07-06 13:22:44 +07:00
|
|
|
}
|
2005-05-06 06:24:04 +00:00
|
|
|
|
2006-08-25 16:03:54 +00:00
|
|
|
/**
|
2011-02-03 18:06:10 +00:00
|
|
|
* Returns an unfinished attempt (if there is one) for the given
|
|
|
|
* user on the given quiz. This function does not return preview attempts.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $quizid the id of the quiz.
|
|
|
|
* @param int $userid the id of the user.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
|
|
|
* @return mixed the unfinished attempt if there is one, false if not.
|
2006-08-25 16:03:54 +00:00
|
|
|
*/
|
2005-07-02 11:42:42 +00:00
|
|
|
function quiz_get_user_attempt_unfinished($quizid, $userid) {
|
2007-07-13 15:58:42 +00:00
|
|
|
$attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true);
|
2006-12-22 12:52:13 +00:00
|
|
|
if ($attempts) {
|
|
|
|
return array_shift($attempts);
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
2005-05-20 11:44:59 +00:00
|
|
|
}
|
|
|
|
|
2007-06-18 16:19:00 +00:00
|
|
|
/**
|
|
|
|
* Delete a quiz attempt.
|
2011-05-12 00:30:25 +01:00
|
|
|
* @param mixed $attempt an integer attempt id or an attempt object
|
|
|
|
* (row of the quiz_attempts table).
|
2008-04-23 14:31:21 +00:00
|
|
|
* @param object $quiz the quiz object.
|
2007-06-18 16:19:00 +00:00
|
|
|
*/
|
|
|
|
function quiz_delete_attempt($attempt, $quiz) {
|
2008-06-09 10:00:35 +00:00
|
|
|
global $DB;
|
2007-06-18 16:19:00 +00:00
|
|
|
if (is_numeric($attempt)) {
|
2008-06-09 10:00:35 +00:00
|
|
|
if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) {
|
2007-06-18 16:19:00 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2007-08-09 21:51:09 +00:00
|
|
|
|
2007-06-18 16:19:00 +00:00
|
|
|
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;
|
|
|
|
}
|
2007-08-09 21:51:09 +00:00
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
question_engine::delete_questions_usage_by_activity($attempt->uniqueid);
|
2011-03-16 18:56:14 +00:00
|
|
|
$DB->delete_records('quiz_attempts', array('id' => $attempt->id));
|
2007-06-18 16:19:00 +00:00
|
|
|
|
|
|
|
// Search quiz_attempts for other instances by this user.
|
|
|
|
// If none, then delete record for this quiz, this user from quiz_grades
|
2012-05-04 12:40:21 +01:00
|
|
|
// else recalculate best grade.
|
2007-06-18 16:19:00 +00:00
|
|
|
$userid = $attempt->userid;
|
2008-06-09 10:00:35 +00:00
|
|
|
if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) {
|
2011-05-12 00:30:25 +01:00
|
|
|
$DB->delete_records('quiz_grades', array('userid' => $userid, 'quiz' => $quiz->id));
|
2007-06-18 16:19:00 +00:00
|
|
|
} else {
|
|
|
|
quiz_save_best_grade($quiz, $userid);
|
|
|
|
}
|
2007-09-17 16:17:24 +00:00
|
|
|
|
|
|
|
quiz_update_grades($quiz, $userid);
|
2007-06-18 16:19:00 +00:00
|
|
|
}
|
|
|
|
|
2009-02-27 08:45:05 +00:00
|
|
|
/**
|
|
|
|
* Delete all the preview attempts at a quiz, or possibly all the attempts belonging
|
|
|
|
* to one user.
|
|
|
|
* @param object $quiz the quiz object.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $userid (optional) if given, only delete the previews belonging to this user.
|
2009-02-27 08:45:05 +00:00
|
|
|
*/
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
|
|
|
/**
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $quizid The quiz id.
|
|
|
|
* @return bool whether this quiz has any (non-preview) attempts.
|
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
|
|
|
*/
|
|
|
|
function quiz_has_attempts($quizid) {
|
|
|
|
global $DB;
|
|
|
|
return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0));
|
|
|
|
}
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Functions to do with quiz layout and pages //////////////////////////////////
|
2005-07-02 11:42:42 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* Returns a comma separated list of question ids for the quiz
|
|
|
|
*
|
2008-11-23 12:46:51 +00:00
|
|
|
* @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.
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2005-05-06 06:24:04 +00:00
|
|
|
function quiz_questions_in_quiz($layout) {
|
2011-02-08 15:19:48 +00:00
|
|
|
$questions = str_replace(',0', '', quiz_clean_layout($layout, true));
|
|
|
|
if ($questions === '0') {
|
|
|
|
return '';
|
|
|
|
} else {
|
|
|
|
return $questions;
|
|
|
|
}
|
2005-05-06 06:24:04 +00:00
|
|
|
}
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
/**
|
2008-11-23 12:46:51 +00:00
|
|
|
* Returns the number of pages in a quiz layout
|
2007-04-10 14:37:36 +00:00
|
|
|
*
|
2008-11-23 12:46:51 +00:00
|
|
|
* @param string $layout The string representing the quiz layout. Always ends in ,0
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return int The number of pages in the quiz.
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2005-05-06 06:24:04 +00:00
|
|
|
function quiz_number_of_pages($layout) {
|
2011-02-08 15:19:48 +00:00
|
|
|
return substr_count(',' . $layout, ',0');
|
2008-11-20 06:59:11 +00:00
|
|
|
}
|
2011-02-03 18:06:10 +00:00
|
|
|
|
2008-11-20 06:59:11 +00:00
|
|
|
/**
|
|
|
|
* Returns the number of questions in the quiz layout
|
|
|
|
*
|
2008-11-23 12:46:51 +00:00
|
|
|
* @param string $layout the string representing the quiz layout.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return int The number of questions in the quiz.
|
2008-11-20 06:59:11 +00:00
|
|
|
*/
|
|
|
|
function quiz_number_of_questions_in_quiz($layout) {
|
2010-05-01 08:11:25 +00:00
|
|
|
$layout = quiz_questions_in_quiz(quiz_clean_layout($layout));
|
2008-11-23 12:46:51 +00:00
|
|
|
$count = substr_count($layout, ',');
|
|
|
|
if ($layout !== '') {
|
2008-11-20 06:59:11 +00:00
|
|
|
$count++;
|
|
|
|
}
|
|
|
|
return $count;
|
2005-05-06 06:24:04 +00:00
|
|
|
}
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* Re-paginates the quiz layout
|
|
|
|
*
|
2011-07-07 12:37:40 +01:00
|
|
|
* @param string $layout The string representing the quiz layout. If there is
|
|
|
|
* if there is any doubt about the quality of the input data, call
|
|
|
|
* quiz_clean_layout before you call this function.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $perpage The number of questions per page
|
|
|
|
* @param bool $shuffle Should the questions be reordered randomly?
|
2008-11-23 12:46:51 +00:00
|
|
|
* @return string the new layout string
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2009-02-27 08:45:05 +00:00
|
|
|
function quiz_repaginate($layout, $perpage, $shuffle = false) {
|
2011-12-22 18:02:24 +00:00
|
|
|
$questions = quiz_questions_in_quiz($layout);
|
|
|
|
if (!$questions) {
|
|
|
|
return '0';
|
|
|
|
}
|
|
|
|
|
|
|
|
$questions = explode(',', quiz_questions_in_quiz($layout));
|
2005-05-06 06:24:04 +00:00
|
|
|
if ($shuffle) {
|
|
|
|
shuffle($questions);
|
|
|
|
}
|
2011-12-22 18:02:24 +00:00
|
|
|
|
|
|
|
$onthispage = 0;
|
|
|
|
$layout = array();
|
2005-05-06 06:24:04 +00:00
|
|
|
foreach ($questions as $question) {
|
2011-12-22 18:02:24 +00:00
|
|
|
if ($perpage and $onthispage >= $perpage) {
|
|
|
|
$layout[] = 0;
|
|
|
|
$onthispage = 0;
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
2011-12-22 18:02:24 +00:00
|
|
|
$layout[] = $question;
|
|
|
|
$onthispage += 1;
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
2011-12-22 18:02:24 +00:00
|
|
|
|
|
|
|
$layout[] = 0;
|
|
|
|
return implode(',', $layout);
|
2005-05-06 06:24:04 +00:00
|
|
|
}
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Functions to do with quiz grades ////////////////////////////////////////////
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* Creates an array of maximum grades for a quiz
|
2009-11-04 11:58:30 +00:00
|
|
|
*
|
2011-02-03 18:06:10 +00:00
|
|
|
* The grades are extracted from the quiz_question_instances table.
|
|
|
|
* @param object $quiz The quiz settings.
|
|
|
|
* @return array of grades indexed by question id. These are the maximum
|
2008-12-05 05:08:37 +00:00
|
|
|
* possible grades that students can achieve for each of the questions.
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2005-05-06 06:24:04 +00:00
|
|
|
function quiz_get_all_question_grades($quiz) {
|
2008-06-09 10:00:35 +00:00
|
|
|
global $CFG, $DB;
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
$questionlist = quiz_questions_in_quiz($quiz->questions);
|
2005-01-08 20:06:00 +00:00
|
|
|
if (empty($questionlist)) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2008-06-09 10:00:35 +00:00
|
|
|
$params = array($quiz->id);
|
|
|
|
$wheresql = '';
|
|
|
|
if (!is_null($questionlist)) {
|
|
|
|
list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist));
|
2014-01-13 18:25:39 +00:00
|
|
|
$wheresql = ' AND questionid ' . $usql;
|
2008-06-09 10:00:35 +00:00
|
|
|
$params = array_merge($params, $question_params);
|
|
|
|
}
|
|
|
|
|
2014-01-13 18:25:39 +00:00
|
|
|
$instances = $DB->get_records_sql("SELECT questionid, maxmark, id
|
2008-06-09 10:00:35 +00:00
|
|
|
FROM {quiz_question_instances}
|
2014-01-13 18:25:39 +00:00
|
|
|
WHERE quizid = ?{$wheresql}", $params);
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2014-01-13 18:25:39 +00:00
|
|
|
$list = explode(',', $questionlist);
|
2005-01-08 20:06:00 +00:00
|
|
|
$grades = array();
|
|
|
|
|
|
|
|
foreach ($list as $qid) {
|
2005-05-06 06:24:04 +00:00
|
|
|
if (isset($instances[$qid])) {
|
2014-01-13 18:25:39 +00:00
|
|
|
$grades[$qid] = $instances[$qid]->maxmark;
|
2005-01-08 20:06:00 +00:00
|
|
|
} else {
|
|
|
|
$grades[$qid] = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $grades;
|
|
|
|
}
|
|
|
|
|
2006-08-22 17:31:26 +00:00
|
|
|
/**
|
|
|
|
* Convert the raw grade stored in $attempt into a grade out of the maximum
|
|
|
|
* grade for this quiz.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
2006-08-22 17:31:26 +00:00
|
|
|
* @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
|
2011-02-03 18:06:10 +00:00
|
|
|
* @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param bool|string $format whether to format the results for display
|
2011-02-03 18:06:10 +00:00
|
|
|
* or 'question' to format a question grade (different number of decimal places.
|
2011-05-12 00:30:25 +01:00
|
|
|
* @return float|string the rescaled grade, or null/the lang string 'notyetgraded'
|
|
|
|
* if the $grade is null.
|
2006-08-22 17:31:26 +00:00
|
|
|
*/
|
2011-02-03 18:06:10 +00:00
|
|
|
function quiz_rescale_grade($rawgrade, $quiz, $format = true) {
|
|
|
|
if (is_null($rawgrade)) {
|
|
|
|
$grade = null;
|
|
|
|
} else if ($quiz->sumgrades >= 0.000005) {
|
2007-12-18 18:20:25 +00:00
|
|
|
$grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
|
2006-08-22 17:31:26 +00:00
|
|
|
} else {
|
2007-12-18 18:20:25 +00:00
|
|
|
$grade = 0;
|
2006-08-22 17:31:26 +00:00
|
|
|
}
|
2011-02-03 18:06:10 +00:00
|
|
|
if ($format === 'question') {
|
|
|
|
$grade = quiz_format_question_grade($quiz, $grade);
|
|
|
|
} else if ($format) {
|
|
|
|
$grade = quiz_format_grade($quiz, $grade);
|
|
|
|
}
|
2007-12-18 18:20:25 +00:00
|
|
|
return $grade;
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
/**
|
2006-08-22 17:31:26 +00:00
|
|
|
* Get the feedback text that should be show to a student who
|
2007-07-30 10:33:43 +00:00
|
|
|
* got this grade on this quiz. The feedback is processed ready for diplay.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
2006-08-22 17:31:26 +00:00
|
|
|
* @param float $grade a grade on this quiz.
|
2011-02-11 18:47:08 +00:00
|
|
|
* @param object $quiz the quiz settings.
|
|
|
|
* @param object $context the quiz context.
|
2006-08-22 17:31:26 +00:00
|
|
|
* @return string the comment that corresponds to this grade (empty string if there is not one.
|
|
|
|
*/
|
2011-02-11 18:47:08 +00:00
|
|
|
function quiz_feedback_for_grade($grade, $quiz, $context) {
|
2008-06-09 10:00:35 +00:00
|
|
|
global $DB;
|
2006-08-22 17:31:26 +00:00
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
if (is_null($grade)) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2012-01-18 17:32:00 +00:00
|
|
|
// With CBM etc, it is possible to get -ve grades, which would then not match
|
|
|
|
// any feedback. Therefore, we replace -ve grades with 0.
|
|
|
|
$grade = max($grade, 0);
|
|
|
|
|
2011-02-11 18:47:08 +00:00
|
|
|
$feedback = $DB->get_record_select('quiz_feedback',
|
|
|
|
'quizid = ? AND mingrade <= ? AND ? < maxgrade', array($quiz->id, $grade, $grade));
|
2010-08-10 09:56:48 +00:00
|
|
|
|
|
|
|
if (empty($feedback->feedbacktext)) {
|
2011-02-11 18:47:08 +00:00
|
|
|
return '';
|
2006-08-22 17:31:26 +00:00
|
|
|
}
|
2006-12-22 12:52:13 +00:00
|
|
|
|
2007-07-30 10:33:43 +00:00
|
|
|
// Clean the text, ready for display.
|
2011-02-21 18:10:19 +00:00
|
|
|
$formatoptions = new stdClass();
|
2007-07-30 10:33:43 +00:00
|
|
|
$formatoptions->noclean = true;
|
2011-05-12 00:30:25 +01:00
|
|
|
$feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php',
|
|
|
|
$context->id, 'mod_quiz', 'feedback', $feedback->id);
|
2010-08-10 09:56:48 +00:00
|
|
|
$feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
|
2007-07-30 10:33:43 +00:00
|
|
|
|
2010-08-10 09:56:48 +00:00
|
|
|
return $feedbacktext;
|
2006-08-22 17:31:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2009-12-21 16:39:06 +00:00
|
|
|
* @param object $quiz the quiz database row.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return bool Whether this quiz has any non-blank feedback text.
|
2006-08-22 17:31:26 +00:00
|
|
|
*/
|
2008-09-23 07:18:15 +00:00
|
|
|
function quiz_has_feedback($quiz) {
|
2008-06-09 10:00:35 +00:00
|
|
|
global $DB;
|
2006-08-22 17:31:26 +00:00
|
|
|
static $cache = array();
|
2008-09-23 07:18:15 +00:00
|
|
|
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));
|
2006-08-22 17:31:26 +00:00
|
|
|
}
|
2008-09-23 07:18:15 +00:00
|
|
|
return $cache[$quiz->id];
|
2006-08-22 17:31:26 +00:00
|
|
|
}
|
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
2012-01-11 16:21:47 +00:00
|
|
|
* You should call {@link quiz_delete_previews()} before you call this function.
|
|
|
|
*
|
2011-02-03 18:06:10 +00:00
|
|
|
* @param object $quiz a quiz.
|
|
|
|
*/
|
|
|
|
function quiz_update_sumgrades($quiz) {
|
|
|
|
global $DB;
|
2012-01-11 16:21:47 +00:00
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
$sql = 'UPDATE {quiz}
|
|
|
|
SET sumgrades = COALESCE((
|
2014-01-13 18:25:39 +00:00
|
|
|
SELECT SUM(maxmark)
|
2011-02-03 18:06:10 +00:00
|
|
|
FROM {quiz_question_instances}
|
2014-01-13 18:25:39 +00:00
|
|
|
WHERE quizid = {quiz}.id
|
2011-02-03 18:06:10 +00:00
|
|
|
), 0)
|
|
|
|
WHERE id = ?';
|
2011-02-11 17:36:30 +00:00
|
|
|
$DB->execute($sql, array($quiz->id));
|
|
|
|
$quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', array('id' => $quiz->id));
|
2012-01-11 16:21:47 +00:00
|
|
|
|
|
|
|
if ($quiz->sumgrades < 0.000005 && quiz_has_attempts($quiz->id)) {
|
|
|
|
// If the quiz has been attempted, and the sumgrades has been
|
|
|
|
// set to 0, then we must also set the maximum possible grade to 0, or
|
|
|
|
// we will get a divide by zero error.
|
2011-02-03 18:06:10 +00:00
|
|
|
quiz_set_grade(0, $quiz);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-11 16:21:47 +00:00
|
|
|
/**
|
|
|
|
* Update the sumgrades field of the attempts at a quiz.
|
|
|
|
*
|
|
|
|
* @param object $quiz a quiz.
|
|
|
|
*/
|
2011-02-03 18:06:10 +00:00
|
|
|
function quiz_update_all_attempt_sumgrades($quiz) {
|
|
|
|
global $DB;
|
|
|
|
$dm = new question_engine_data_mapper();
|
|
|
|
$timenow = time();
|
|
|
|
|
|
|
|
$sql = "UPDATE {quiz_attempts}
|
|
|
|
SET
|
|
|
|
timemodified = :timenow,
|
|
|
|
sumgrades = (
|
|
|
|
{$dm->sum_usage_marks_subquery('uniqueid')}
|
|
|
|
)
|
2012-04-24 15:01:12 +01:00
|
|
|
WHERE quiz = :quizid AND state = :finishedstate";
|
|
|
|
$DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id,
|
|
|
|
'finishedstate' => quiz_attempt::FINISHED));
|
2011-02-03 18:06:10 +00:00
|
|
|
}
|
|
|
|
|
2006-08-22 17:31:26 +00:00
|
|
|
/**
|
2011-02-09 16:56:44 +00:00
|
|
|
* The quiz grade is the maximum that student's results are marked out of. When it
|
2006-08-22 17:31:26 +00:00
|
|
|
* changes, the corresponding data in quiz_grades and quiz_feedback needs to be
|
2011-02-03 18:06:10 +00:00
|
|
|
* rescaled. After calling this function, you probably need to call
|
|
|
|
* quiz_update_all_attempt_sumgrades, quiz_update_all_final_grades and
|
|
|
|
* quiz_update_grades.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
2006-08-22 17:31:26 +00:00
|
|
|
* @param float $newgrade the new maximum grade for the quiz.
|
2011-05-12 00:30:25 +01:00
|
|
|
* @param object $quiz the quiz we are updating. Passed by reference so its
|
|
|
|
* grade field can be updated too.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return bool indicating success or failure.
|
2006-08-22 17:31:26 +00:00
|
|
|
*/
|
2011-02-03 18:06:10 +00:00
|
|
|
function quiz_set_grade($newgrade, $quiz) {
|
2008-06-09 10:00:35 +00:00
|
|
|
global $DB;
|
2006-08-22 17:31:26 +00:00
|
|
|
// This is potentially expensive, so only do it if necessary.
|
|
|
|
if (abs($quiz->grade - $newgrade) < 1e-7) {
|
|
|
|
// Nothing to do.
|
|
|
|
return true;
|
|
|
|
}
|
2006-10-04 10:21:23 +00:00
|
|
|
|
2012-05-03 22:33:36 +01:00
|
|
|
$oldgrade = $quiz->grade;
|
|
|
|
$quiz->grade = $newgrade;
|
|
|
|
|
2006-08-22 17:31:26 +00:00
|
|
|
// Use a transaction, so that on those databases that support it, this is safer.
|
2009-11-07 08:52:56 +00:00
|
|
|
$transaction = $DB->start_delegated_transaction();
|
|
|
|
|
2011-06-07 16:27:43 +01:00
|
|
|
// Update the quiz table.
|
|
|
|
$DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
|
|
|
|
|
2012-05-03 22:33:36 +01:00
|
|
|
if ($oldgrade < 1) {
|
|
|
|
// If the old grade was zero, we cannot rescale, we have to recompute.
|
|
|
|
// We also recompute if the old grade was too small to avoid underflow problems.
|
|
|
|
quiz_update_all_final_grades($quiz);
|
2011-06-07 16:27:43 +01:00
|
|
|
|
2012-05-03 22:33:36 +01:00
|
|
|
} else {
|
|
|
|
// We can rescale the grades efficiently.
|
2011-06-07 16:27:43 +01:00
|
|
|
$timemodified = time();
|
|
|
|
$DB->execute("
|
|
|
|
UPDATE {quiz_grades}
|
|
|
|
SET grade = ? * grade, timemodified = ?
|
|
|
|
WHERE quiz = ?
|
2012-05-03 22:33:36 +01:00
|
|
|
", array($newgrade/$oldgrade, $timemodified, $quiz->id));
|
|
|
|
}
|
2011-06-07 16:27:43 +01:00
|
|
|
|
2012-05-03 22:33:36 +01:00
|
|
|
if ($oldgrade > 1e-7) {
|
2011-06-07 16:27:43 +01:00
|
|
|
// Update the quiz_feedback table.
|
2012-05-03 22:33:36 +01:00
|
|
|
$factor = $newgrade/$oldgrade;
|
2011-06-07 16:27:43 +01:00
|
|
|
$DB->execute("
|
|
|
|
UPDATE {quiz_feedback}
|
|
|
|
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
|
|
|
|
WHERE quizid = ?
|
|
|
|
", array($factor, $factor, $quiz->id));
|
|
|
|
}
|
2006-12-22 12:52:13 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Update grade item and send all grades to gradebook.
|
2011-06-07 16:27:43 +01:00
|
|
|
quiz_grade_item_update($quiz);
|
|
|
|
quiz_update_grades($quiz);
|
2007-06-10 22:52:41 +00:00
|
|
|
|
2011-06-07 16:27:43 +01:00
|
|
|
$transaction->allow_commit();
|
|
|
|
return true;
|
2006-08-22 17:31:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $userid The userid to calculate the grade for. Defaults to the current user.
|
2008-07-11 07:27:14 +00:00
|
|
|
* @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.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return bool Indicates success or failure.
|
2006-08-22 17:31:26 +00:00
|
|
|
*/
|
2008-07-11 07:27:14 +00:00
|
|
|
function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
|
2012-01-30 17:47:11 +00:00
|
|
|
global $DB, $OUTPUT, $USER;
|
2005-05-06 06:24:04 +00:00
|
|
|
|
2006-08-22 17:31:26 +00:00
|
|
|
if (empty($userid)) {
|
2005-05-06 06:24:04 +00:00
|
|
|
$userid = $USER->id;
|
|
|
|
}
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2011-05-12 00:30:25 +01:00
|
|
|
if (!$attempts) {
|
2012-05-04 12:40:21 +01:00
|
|
|
// Get all the attempts made by the user.
|
2011-02-03 18:06:10 +00:00
|
|
|
$attempts = quiz_get_user_attempts($quiz->id, $userid);
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Calculate the best grade.
|
2005-01-08 20:06:00 +00:00
|
|
|
$bestgrade = quiz_calculate_best_grade($quiz, $attempts);
|
quiz settings: MDL-18485 Improve quiz settings form
* Reorder form fields to group things more logically.
** and on the corresponding admin page too.
* Set some options to be 'Advanced' by default:
** Apply penalties.
** Each attempt builds on the last.
** Decimal places for question grades.
** The five 'Extra restrictions on attempts' settings. (password, etc.)
* Admins can still change this to suit their institiution at Administration > Plugins > Activity modules > Quiz.
* These new defaults are applied if the admin had not previously set any fields to be advanced.
* Disable some filds when they are not applicable:
** Grading method, if num attempts = 1
** Penaly scheme, if adaptive mode = no
** Each attempt builds of last, if num attempts = 1
** Review after quiz closed options, if no close date.
** Delay between 1st and 2nd attempts, if num attempts = 1
** Delay between later attempts, if num attempts < 3
* Convert quiz.timelimit to be in seconds, for consistency, and ready for the new duration field type (MDL 18500).
** Including ensuring that backup and restore is backwards compatible.
* MDL-5537 New setting, questiondecimalpoints, so, for example, you can show the quiz grade as an integer, but have fractional question grades.
** There is a 'Same as overall decimal points' option, which is the default.
* Improve some field labels.
* Make corresponding changes in the help files.
2009-03-10 08:39:51 +00:00
|
|
|
$bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
|
2006-12-22 12:52:13 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Save the best grade in the database.
|
2011-02-03 18:06:10 +00:00
|
|
|
if (is_null($bestgrade)) {
|
|
|
|
$DB->delete_records('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid));
|
|
|
|
|
2011-05-12 00:30:25 +01:00
|
|
|
} else if ($grade = $DB->get_record('quiz_grades',
|
|
|
|
array('quiz' => $quiz->id, 'userid' => $userid))) {
|
2005-06-05 20:51:15 +00:00
|
|
|
$grade->grade = $bestgrade;
|
2005-01-08 20:06:00 +00:00
|
|
|
$grade->timemodified = time();
|
2009-06-13 17:47:18 +00:00
|
|
|
$DB->update_record('quiz_grades', $grade);
|
2011-02-03 18:06:10 +00:00
|
|
|
|
2005-01-08 20:06:00 +00:00
|
|
|
} else {
|
2012-04-26 19:02:43 +01:00
|
|
|
$grade = new stdClass();
|
2005-01-08 20:06:00 +00:00
|
|
|
$grade->quiz = $quiz->id;
|
|
|
|
$grade->userid = $userid;
|
2005-06-05 20:51:15 +00:00
|
|
|
$grade->grade = $bestgrade;
|
2005-01-08 20:06:00 +00:00
|
|
|
$grade->timemodified = time();
|
2009-06-13 17:47:18 +00:00
|
|
|
$DB->insert_record('quiz_grades', $grade);
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
2007-06-10 22:52:41 +00:00
|
|
|
|
|
|
|
quiz_update_grades($quiz, $userid);
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
|
|
|
|
2005-06-05 20:51:15 +00:00
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* Calculate the overall grade for a quiz given a number of attempts by a particular user.
|
|
|
|
*
|
2011-07-07 13:11:00 +01:00
|
|
|
* @param object $quiz the quiz settings object.
|
|
|
|
* @param array $attempts an array of all the user's attempts at this quiz in order.
|
|
|
|
* @return float the overall grade
|
2007-04-10 14:37:36 +00:00
|
|
|
*/
|
2005-01-08 20:06:00 +00:00
|
|
|
function quiz_calculate_best_grade($quiz, $attempts) {
|
|
|
|
|
|
|
|
switch ($quiz->grademethod) {
|
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_ATTEMPTFIRST:
|
2011-07-07 13:11:00 +01:00
|
|
|
$firstattempt = reset($attempts);
|
|
|
|
return $firstattempt->sumgrades;
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_ATTEMPTLAST:
|
2011-07-07 13:11:00 +01:00
|
|
|
$lastattempt = end($attempts);
|
|
|
|
return $lastattempt->sumgrades;
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_GRADEAVERAGE:
|
2005-01-08 20:06:00 +00:00
|
|
|
$sum = 0;
|
|
|
|
$count = 0;
|
|
|
|
foreach ($attempts as $attempt) {
|
2011-02-03 18:06:10 +00:00
|
|
|
if (!is_null($attempt->sumgrades)) {
|
|
|
|
$sum += $attempt->sumgrades;
|
|
|
|
$count++;
|
|
|
|
}
|
2005-01-08 20:06:00 +00:00
|
|
|
}
|
2011-02-03 18:06:10 +00:00
|
|
|
if ($count == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $sum / $count;
|
2005-01-08 20:06:00 +00:00
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_GRADEHIGHEST:
|
2011-07-07 13:11:00 +01:00
|
|
|
default:
|
2011-02-03 18:06:10 +00:00
|
|
|
$max = null;
|
2005-01-08 20:06:00 +00:00
|
|
|
foreach ($attempts as $attempt) {
|
|
|
|
if ($attempt->sumgrades > $max) {
|
|
|
|
$max = $attempt->sumgrades;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $max;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
/**
|
|
|
|
* Update the final grade at this quiz for all students.
|
|
|
|
*
|
|
|
|
* This function is equivalent to calling quiz_save_best_grade for all
|
|
|
|
* users, but much more efficient.
|
|
|
|
*
|
|
|
|
* @param object $quiz the quiz settings.
|
|
|
|
*/
|
|
|
|
function quiz_update_all_final_grades($quiz) {
|
|
|
|
global $DB;
|
|
|
|
|
|
|
|
if (!$quiz->sumgrades) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-04-24 15:01:12 +01:00
|
|
|
$param = array('iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED);
|
2011-02-03 18:06:10 +00:00
|
|
|
$firstlastattemptjoin = "JOIN (
|
|
|
|
SELECT
|
|
|
|
iquiza.userid,
|
|
|
|
MIN(attempt) AS firstattempt,
|
|
|
|
MAX(attempt) AS lastattempt
|
|
|
|
|
2011-06-18 11:36:54 +01:00
|
|
|
FROM {quiz_attempts} iquiza
|
2011-02-03 18:06:10 +00:00
|
|
|
|
|
|
|
WHERE
|
2012-05-03 19:45:18 +01:00
|
|
|
iquiza.state = :istatefinished AND
|
2011-02-03 18:06:10 +00:00
|
|
|
iquiza.preview = 0 AND
|
|
|
|
iquiza.quiz = :iquizid
|
|
|
|
|
|
|
|
GROUP BY iquiza.userid
|
|
|
|
) first_last_attempts ON first_last_attempts.userid = quiza.userid";
|
|
|
|
|
|
|
|
switch ($quiz->grademethod) {
|
|
|
|
case QUIZ_ATTEMPTFIRST:
|
2011-07-06 14:19:25 +01:00
|
|
|
// Because of the where clause, there will only be one row, but we
|
2011-02-03 18:06:10 +00:00
|
|
|
// must still use an aggregate function.
|
|
|
|
$select = 'MAX(quiza.sumgrades)';
|
|
|
|
$join = $firstlastattemptjoin;
|
|
|
|
$where = 'quiza.attempt = first_last_attempts.firstattempt AND';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case QUIZ_ATTEMPTLAST:
|
2011-07-06 14:19:25 +01:00
|
|
|
// Because of the where clause, there will only be one row, but we
|
2011-02-03 18:06:10 +00:00
|
|
|
// must still use an aggregate function.
|
|
|
|
$select = 'MAX(quiza.sumgrades)';
|
|
|
|
$join = $firstlastattemptjoin;
|
|
|
|
$where = 'quiza.attempt = first_last_attempts.lastattempt AND';
|
|
|
|
break;
|
|
|
|
|
|
|
|
case QUIZ_GRADEAVERAGE:
|
|
|
|
$select = 'AVG(quiza.sumgrades)';
|
|
|
|
$join = '';
|
|
|
|
$where = '';
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
case QUIZ_GRADEHIGHEST:
|
|
|
|
$select = 'MAX(quiza.sumgrades)';
|
|
|
|
$join = '';
|
|
|
|
$where = '';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($quiz->sumgrades >= 0.000005) {
|
|
|
|
$finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades);
|
|
|
|
} else {
|
|
|
|
$finalgrade = '0';
|
|
|
|
}
|
|
|
|
$param['quizid'] = $quiz->id;
|
|
|
|
$param['quizid2'] = $quiz->id;
|
|
|
|
$param['quizid3'] = $quiz->id;
|
|
|
|
$param['quizid4'] = $quiz->id;
|
2012-04-24 15:01:12 +01:00
|
|
|
$param['statefinished'] = quiz_attempt::FINISHED;
|
|
|
|
$param['statefinished2'] = quiz_attempt::FINISHED;
|
2011-02-03 18:06:10 +00:00
|
|
|
$finalgradesubquery = "
|
|
|
|
SELECT quiza.userid, $finalgrade AS newgrade
|
|
|
|
FROM {quiz_attempts} quiza
|
|
|
|
$join
|
|
|
|
WHERE
|
|
|
|
$where
|
2012-05-03 12:02:29 +01:00
|
|
|
quiza.state = :statefinished AND
|
2011-02-03 18:06:10 +00:00
|
|
|
quiza.preview = 0 AND
|
|
|
|
quiza.quiz = :quizid3
|
|
|
|
GROUP BY quiza.userid";
|
|
|
|
|
|
|
|
$changedgrades = $DB->get_records_sql("
|
|
|
|
SELECT users.userid, qg.id, qg.grade, newgrades.newgrade
|
|
|
|
|
|
|
|
FROM (
|
|
|
|
SELECT userid
|
|
|
|
FROM {quiz_grades} qg
|
|
|
|
WHERE quiz = :quizid
|
|
|
|
UNION
|
|
|
|
SELECT DISTINCT userid
|
|
|
|
FROM {quiz_attempts} quiza2
|
|
|
|
WHERE
|
2012-05-03 12:02:29 +01:00
|
|
|
quiza2.state = :statefinished2 AND
|
2011-02-03 18:06:10 +00:00
|
|
|
quiza2.preview = 0 AND
|
|
|
|
quiza2.quiz = :quizid2
|
|
|
|
) users
|
|
|
|
|
|
|
|
LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4
|
|
|
|
|
|
|
|
LEFT JOIN (
|
|
|
|
$finalgradesubquery
|
|
|
|
) newgrades ON newgrades.userid = users.userid
|
|
|
|
|
|
|
|
WHERE
|
|
|
|
ABS(newgrades.newgrade - qg.grade) > 0.000005 OR
|
2011-11-04 15:02:29 +00:00
|
|
|
((newgrades.newgrade IS NULL OR qg.grade IS NULL) AND NOT
|
|
|
|
(newgrades.newgrade IS NULL AND qg.grade IS NULL))",
|
|
|
|
// The mess on the previous line is detecting where the value is
|
|
|
|
// NULL in one column, and NOT NULL in the other, but SQL does
|
|
|
|
// not have an XOR operator, and MS SQL server can't cope with
|
|
|
|
// (newgrades.newgrade IS NULL) <> (qg.grade IS NULL).
|
2011-02-11 17:36:30 +00:00
|
|
|
$param);
|
2011-02-03 18:06:10 +00:00
|
|
|
|
|
|
|
$timenow = time();
|
|
|
|
$todelete = array();
|
|
|
|
foreach ($changedgrades as $changedgrade) {
|
|
|
|
|
|
|
|
if (is_null($changedgrade->newgrade)) {
|
|
|
|
$todelete[] = $changedgrade->userid;
|
|
|
|
|
|
|
|
} else if (is_null($changedgrade->grade)) {
|
2011-02-21 18:10:19 +00:00
|
|
|
$toinsert = new stdClass();
|
2011-02-03 18:06:10 +00:00
|
|
|
$toinsert->quiz = $quiz->id;
|
|
|
|
$toinsert->userid = $changedgrade->userid;
|
|
|
|
$toinsert->timemodified = $timenow;
|
|
|
|
$toinsert->grade = $changedgrade->newgrade;
|
|
|
|
$DB->insert_record('quiz_grades', $toinsert);
|
|
|
|
|
|
|
|
} else {
|
2011-02-21 18:10:19 +00:00
|
|
|
$toupdate = new stdClass();
|
2011-02-03 18:06:10 +00:00
|
|
|
$toupdate->id = $changedgrade->id;
|
|
|
|
$toupdate->grade = $changedgrade->newgrade;
|
|
|
|
$toupdate->timemodified = $timenow;
|
|
|
|
$DB->update_record('quiz_grades', $toupdate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($todelete)) {
|
2011-03-30 11:00:33 +01:00
|
|
|
list($test, $params) = $DB->get_in_or_equal($todelete);
|
2011-03-30 14:30:40 +01:00
|
|
|
$DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test,
|
|
|
|
array_merge(array($quiz->id), $params));
|
2011-02-03 18:06:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
/**
|
|
|
|
* Efficiently update check state time on all open attempts
|
|
|
|
*
|
|
|
|
* @param array $conditions optional restrictions on which attempts to update
|
|
|
|
* Allowed conditions:
|
|
|
|
* courseid => (array|int) attempts in given course(s)
|
|
|
|
* userid => (array|int) attempts for given user(s)
|
|
|
|
* quizid => (array|int) attempts in given quiz(s)
|
|
|
|
* groupid => (array|int) quizzes with some override for given group(s)
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
function quiz_update_open_attempts(array $conditions) {
|
|
|
|
global $DB;
|
|
|
|
|
|
|
|
foreach ($conditions as &$value) {
|
|
|
|
if (!is_array($value)) {
|
|
|
|
$value = array($value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$params = array();
|
2013-01-18 16:43:48 +00:00
|
|
|
$wheres = array("quiza.state IN ('inprogress', 'overdue')");
|
|
|
|
$iwheres = array("iquiza.state IN ('inprogress', 'overdue')");
|
2012-10-09 13:48:30 -04:00
|
|
|
|
|
|
|
if (isset($conditions['courseid'])) {
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid');
|
|
|
|
$params = array_merge($params, $inparams);
|
2013-01-18 16:43:48 +00:00
|
|
|
$wheres[] = "quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'icid');
|
|
|
|
$params = array_merge($params, $inparams);
|
|
|
|
$iwheres[] = "iquiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
|
2012-10-09 13:48:30 -04:00
|
|
|
}
|
2013-01-18 16:43:48 +00:00
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
if (isset($conditions['userid'])) {
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid');
|
|
|
|
$params = array_merge($params, $inparams);
|
2013-01-18 16:43:48 +00:00
|
|
|
$wheres[] = "quiza.userid $incond";
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'iuid');
|
|
|
|
$params = array_merge($params, $inparams);
|
|
|
|
$iwheres[] = "iquiza.userid $incond";
|
2012-10-09 13:48:30 -04:00
|
|
|
}
|
2013-01-18 16:43:48 +00:00
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
if (isset($conditions['quizid'])) {
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid');
|
|
|
|
$params = array_merge($params, $inparams);
|
2013-01-18 16:43:48 +00:00
|
|
|
$wheres[] = "quiza.quiz $incond";
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'iqid');
|
|
|
|
$params = array_merge($params, $inparams);
|
|
|
|
$iwheres[] = "iquiza.quiz $incond";
|
2012-10-09 13:48:30 -04:00
|
|
|
}
|
2013-01-18 16:43:48 +00:00
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
if (isset($conditions['groupid'])) {
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid');
|
|
|
|
$params = array_merge($params, $inparams);
|
2013-01-18 16:43:48 +00:00
|
|
|
$wheres[] = "quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
|
|
|
|
list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'igid');
|
|
|
|
$params = array_merge($params, $inparams);
|
|
|
|
$iwheres[] = "iquiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
|
2012-10-09 13:48:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// SQL to compute timeclose and timelimit for each attempt:
|
2013-01-18 16:43:48 +00:00
|
|
|
$quizausersql = quiz_get_attempt_usertime_sql(
|
|
|
|
implode("\n AND ", $iwheres));
|
2012-10-09 13:48:30 -04:00
|
|
|
|
|
|
|
// SQL to compute the new timecheckstate
|
|
|
|
$timecheckstatesql = "
|
|
|
|
CASE WHEN quizauser.usertimelimit = 0 AND quizauser.usertimeclose = 0 THEN NULL
|
|
|
|
WHEN quizauser.usertimelimit = 0 THEN quizauser.usertimeclose
|
|
|
|
WHEN quizauser.usertimeclose = 0 THEN quiza.timestart + quizauser.usertimelimit
|
|
|
|
WHEN quiza.timestart + quizauser.usertimelimit < quizauser.usertimeclose THEN quiza.timestart + quizauser.usertimelimit
|
|
|
|
ELSE quizauser.usertimeclose END +
|
|
|
|
CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END";
|
|
|
|
|
|
|
|
// SQL to select which attempts to process
|
2013-01-18 16:43:48 +00:00
|
|
|
$attemptselect = implode("\n AND ", $wheres);
|
2012-10-09 13:48:30 -04:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Each database handles updates with inner joins differently:
|
|
|
|
* - mysql does not allow a FROM clause
|
|
|
|
* - postgres and mssql allow FROM but handle table aliases differently
|
|
|
|
* - oracle requires a subquery
|
|
|
|
*
|
|
|
|
* Different code for each database.
|
|
|
|
*/
|
|
|
|
|
|
|
|
$dbfamily = $DB->get_dbfamily();
|
|
|
|
if ($dbfamily == 'mysql') {
|
|
|
|
$updatesql = "UPDATE {quiz_attempts} quiza
|
|
|
|
JOIN {quiz} quiz ON quiz.id = quiza.quiz
|
|
|
|
JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
|
|
|
|
SET quiza.timecheckstate = $timecheckstatesql
|
|
|
|
WHERE $attemptselect";
|
|
|
|
} else if ($dbfamily == 'postgres') {
|
|
|
|
$updatesql = "UPDATE {quiz_attempts} quiza
|
|
|
|
SET timecheckstate = $timecheckstatesql
|
|
|
|
FROM {quiz} quiz, ( $quizausersql ) quizauser
|
|
|
|
WHERE quiz.id = quiza.quiz
|
|
|
|
AND quizauser.id = quiza.id
|
|
|
|
AND $attemptselect";
|
|
|
|
} else if ($dbfamily == 'mssql') {
|
|
|
|
$updatesql = "UPDATE quiza
|
|
|
|
SET timecheckstate = $timecheckstatesql
|
|
|
|
FROM {quiz_attempts} quiza
|
|
|
|
JOIN {quiz} quiz ON quiz.id = quiza.quiz
|
|
|
|
JOIN ( $quizausersql ) quizauser ON quizauser.id = quiza.id
|
|
|
|
WHERE $attemptselect";
|
|
|
|
} else {
|
|
|
|
// oracle, sqlite and others
|
|
|
|
$updatesql = "UPDATE {quiz_attempts} quiza
|
|
|
|
SET timecheckstate = (
|
|
|
|
SELECT $timecheckstatesql
|
|
|
|
FROM {quiz} quiz, ( $quizausersql ) quizauser
|
|
|
|
WHERE quiz.id = quiza.quiz
|
|
|
|
AND quizauser.id = quiza.id
|
|
|
|
)
|
|
|
|
WHERE $attemptselect";
|
|
|
|
}
|
|
|
|
|
|
|
|
$DB->execute($updatesql, $params);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides.
|
|
|
|
*
|
2013-01-18 16:43:48 +00:00
|
|
|
* @param string $redundantwhereclauses extra where clauses to add to the subquery
|
|
|
|
* for performance. These can use the table alias iquiza for the quiz attempts table.
|
|
|
|
* @return string SQL select with columns attempt.id, usertimeclose, usertimelimit.
|
2012-10-09 13:48:30 -04:00
|
|
|
*/
|
2013-01-18 16:43:48 +00:00
|
|
|
function quiz_get_attempt_usertime_sql($redundantwhereclauses = '') {
|
|
|
|
if ($redundantwhereclauses) {
|
|
|
|
$redundantwhereclauses = 'WHERE ' . $redundantwhereclauses;
|
|
|
|
}
|
2012-10-09 13:48:30 -04:00
|
|
|
// The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede
|
|
|
|
// any other group override
|
|
|
|
$quizausersql = "
|
|
|
|
SELECT iquiza.id,
|
|
|
|
COALESCE(MAX(quo.timeclose), MAX(qgo1.timeclose), MAX(qgo2.timeclose), iquiz.timeclose) AS usertimeclose,
|
|
|
|
COALESCE(MAX(quo.timelimit), MAX(qgo3.timelimit), MAX(qgo4.timelimit), iquiz.timelimit) AS usertimelimit
|
|
|
|
|
|
|
|
FROM {quiz_attempts} iquiza
|
|
|
|
JOIN {quiz} iquiz ON iquiz.id = iquiza.quiz
|
|
|
|
LEFT JOIN {quiz_overrides} quo ON quo.quiz = iquiza.quiz AND quo.userid = iquiza.userid
|
|
|
|
LEFT JOIN {groups_members} gm ON gm.userid = iquiza.userid
|
|
|
|
LEFT JOIN {quiz_overrides} qgo1 ON qgo1.quiz = iquiza.quiz AND qgo1.groupid = gm.groupid AND qgo1.timeclose = 0
|
|
|
|
LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0
|
|
|
|
LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0
|
|
|
|
LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0
|
2013-01-18 16:43:48 +00:00
|
|
|
$redundantwhereclauses
|
2012-10-09 13:48:30 -04:00
|
|
|
GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit";
|
|
|
|
return $quizausersql;
|
|
|
|
}
|
|
|
|
|
2005-06-05 20:51:15 +00:00
|
|
|
/**
|
2007-04-10 14:37:36 +00:00
|
|
|
* 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
|
|
|
|
*/
|
2005-01-08 20:06:00 +00:00
|
|
|
function quiz_calculate_best_attempt($quiz, $attempts) {
|
|
|
|
|
|
|
|
switch ($quiz->grademethod) {
|
|
|
|
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_ATTEMPTFIRST:
|
2005-01-08 20:06:00 +00:00
|
|
|
foreach ($attempts as $attempt) {
|
|
|
|
return $attempt;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
case QUIZ_GRADEAVERAGE: // We need to do something with it.
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_ATTEMPTLAST:
|
2005-01-08 20:06:00 +00:00
|
|
|
foreach ($attempts as $attempt) {
|
|
|
|
$final = $attempt;
|
|
|
|
}
|
|
|
|
return $final;
|
|
|
|
|
|
|
|
default:
|
2005-05-06 06:24:04 +00:00
|
|
|
case QUIZ_GRADEHIGHEST:
|
2005-01-08 20:06:00 +00:00
|
|
|
$max = -1;
|
|
|
|
foreach ($attempts as $attempt) {
|
|
|
|
if ($attempt->sumgrades > $max) {
|
|
|
|
$max = $attempt->sumgrades;
|
|
|
|
$maxattempt = $attempt;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $maxattempt;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
/**
|
2011-11-10 18:27:31 +00:00
|
|
|
* @return array int => lang string the options for calculating the quiz grade
|
|
|
|
* from the individual attempt grades.
|
2011-02-03 18:06:10 +00:00
|
|
|
*/
|
|
|
|
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')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2007-08-06 13:22:59 +00:00
|
|
|
/**
|
2011-05-12 00:30:25 +01:00
|
|
|
* @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
|
|
|
|
* QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
|
2007-08-06 13:22:59 +00:00
|
|
|
* @return the lang string for that option.
|
|
|
|
*/
|
|
|
|
function quiz_get_grading_option_name($option) {
|
|
|
|
$strings = quiz_get_grading_options();
|
|
|
|
return $strings[$option];
|
|
|
|
}
|
|
|
|
|
2011-11-10 18:27:31 +00:00
|
|
|
/**
|
|
|
|
* @return array string => lang string the options for handling overdue quiz
|
|
|
|
* attempts.
|
|
|
|
*/
|
|
|
|
function quiz_get_overdue_handling_options() {
|
|
|
|
return array(
|
|
|
|
'autosubmit' => get_string('overduehandlingautosubmit', 'quiz'),
|
|
|
|
'graceperiod' => get_string('overduehandlinggraceperiod', 'quiz'),
|
|
|
|
'autoabandon' => get_string('overduehandlingautoabandon', 'quiz'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2012-04-24 15:01:12 +01:00
|
|
|
/**
|
|
|
|
* @param string $state one of the state constants like IN_PROGRESS.
|
|
|
|
* @return string the human-readable state name.
|
|
|
|
*/
|
|
|
|
function quiz_attempt_state_name($state) {
|
|
|
|
switch ($state) {
|
|
|
|
case quiz_attempt::IN_PROGRESS:
|
|
|
|
return get_string('stateinprogress', 'quiz');
|
|
|
|
case quiz_attempt::OVERDUE:
|
|
|
|
return get_string('stateoverdue', 'quiz');
|
|
|
|
case quiz_attempt::FINISHED:
|
|
|
|
return get_string('statefinished', 'quiz');
|
|
|
|
case quiz_attempt::ABANDONED:
|
|
|
|
return get_string('stateabandoned', 'quiz');
|
|
|
|
default:
|
|
|
|
throw new coding_exception('Unknown quiz attempt state.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Other quiz functions ////////////////////////////////////////////////////////
|
2005-05-06 06:24:04 +00:00
|
|
|
|
2008-06-15 11:52:07 +00:00
|
|
|
/**
|
2009-02-11 10:08:07 +00:00
|
|
|
* @param object $quiz the quiz.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $cmid the course_module object for this quiz.
|
2009-02-11 10:08:07 +00:00
|
|
|
* @param object $question the question.
|
2008-06-15 11:52:07 +00:00
|
|
|
* @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.
|
|
|
|
*/
|
2009-02-11 10:08:07 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $cmid the course_module.id for this quiz.
|
2009-02-11 10:08:07 +00:00
|
|
|
* @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.
|
2011-05-12 00:30:25 +01:00
|
|
|
* @return the HTML for an edit icon, view icon, or nothing for a question
|
|
|
|
* (depending on permissions).
|
2009-02-11 10:08:07 +00:00
|
|
|
*/
|
2010-08-11 19:38:16 +00:00
|
|
|
function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') {
|
2009-07-03 07:55:30 +00:00
|
|
|
global $CFG, $OUTPUT;
|
2009-02-11 10:08:07 +00:00
|
|
|
|
|
|
|
// Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
|
2008-06-15 11:52:07 +00:00
|
|
|
static $stredit = null;
|
|
|
|
static $strview = null;
|
2011-05-12 00:30:25 +01:00
|
|
|
if ($stredit === null) {
|
2008-06-15 11:52:07 +00:00
|
|
|
$stredit = get_string('edit');
|
|
|
|
$strview = get_string('view');
|
|
|
|
}
|
2009-02-11 10:08:07 +00:00
|
|
|
|
|
|
|
// What sort of icon should we show?
|
|
|
|
$action = '';
|
2011-05-12 00:30:25 +01:00
|
|
|
if (!empty($question->id) &&
|
|
|
|
(question_has_capability_on($question, 'edit', $question->category) ||
|
|
|
|
question_has_capability_on($question, 'move', $question->category))) {
|
2009-02-11 10:08:07 +00:00
|
|
|
$action = $stredit;
|
|
|
|
$icon = '/t/edit';
|
2011-05-12 00:30:25 +01:00
|
|
|
} else if (!empty($question->id) &&
|
|
|
|
question_has_capability_on($question, 'view', $question->category)) {
|
2009-02-11 10:08:07 +00:00
|
|
|
$action = $strview;
|
|
|
|
$icon = '/i/info';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build the icon.
|
|
|
|
if ($action) {
|
2011-06-07 16:46:24 +01:00
|
|
|
if ($returnurl instanceof moodle_url) {
|
2011-08-24 15:31:56 +01:00
|
|
|
$returnurl = $returnurl->out_as_local_url(false);
|
2011-06-07 16:46:24 +01:00
|
|
|
}
|
2009-02-11 10:08:07 +00:00
|
|
|
$questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id);
|
|
|
|
$questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams);
|
2012-01-09 16:33:53 +00:00
|
|
|
return '<a title="' . $action . '" href="' . $questionurl->out() . '" class="questioneditbutton"><img src="' .
|
2010-08-11 19:38:16 +00:00
|
|
|
$OUTPUT->pix_url($icon) . '" alt="' . $action . '" />' . $contentaftericon .
|
|
|
|
'</a>';
|
2012-01-09 16:33:53 +00:00
|
|
|
} else if ($contentaftericon) {
|
|
|
|
return '<span class="questioneditbutton">' . $contentaftericon . '</span>';
|
2009-02-11 10:08:07 +00:00
|
|
|
} else {
|
2012-01-09 16:33:53 +00:00
|
|
|
return '';
|
2008-06-15 11:52:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2007-04-24 16:59:32 +00:00
|
|
|
/**
|
2011-05-27 11:49:23 +01:00
|
|
|
* @param object $quiz the quiz settings
|
|
|
|
* @param object $question the question
|
|
|
|
* @return moodle_url to preview this question with the options from this quiz.
|
|
|
|
*/
|
|
|
|
function quiz_question_preview_url($quiz, $question) {
|
|
|
|
// Get the appropriate display options.
|
|
|
|
$displayoptions = mod_quiz_display_options::make_from_quiz($quiz,
|
|
|
|
mod_quiz_display_options::DURING);
|
|
|
|
|
|
|
|
$maxmark = null;
|
|
|
|
if (isset($question->maxmark)) {
|
|
|
|
$maxmark = $question->maxmark;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Work out the correcte preview URL.
|
|
|
|
return question_preview_url($question->id, $quiz->preferredbehaviour,
|
|
|
|
$maxmark, $displayoptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param object $quiz the quiz settings
|
2007-04-24 16:59:32 +00:00
|
|
|
* @param object $question the question
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param bool $label if true, show the preview question label after the icon
|
2007-04-24 16:59:32 +00:00
|
|
|
* @return the HTML for a preview question icon.
|
|
|
|
*/
|
2009-02-11 10:08:07 +00:00
|
|
|
function quiz_question_preview_button($quiz, $question, $label = false) {
|
2011-06-29 14:39:16 +01:00
|
|
|
global $CFG, $OUTPUT;
|
2009-02-11 10:08:07 +00:00
|
|
|
if (!question_has_capability_on($question, 'use', $question->category)) {
|
2007-08-09 21:51:09 +00:00
|
|
|
return '';
|
|
|
|
}
|
2009-02-11 10:08:07 +00:00
|
|
|
|
2011-05-27 11:49:23 +01:00
|
|
|
$url = quiz_question_preview_url($quiz, $question);
|
2009-02-11 10:08:07 +00:00
|
|
|
|
|
|
|
// Do we want a label?
|
2011-02-03 18:06:10 +00:00
|
|
|
$strpreviewlabel = '';
|
2009-02-11 10:08:07 +00:00
|
|
|
if ($label) {
|
2011-02-03 18:06:10 +00:00
|
|
|
$strpreviewlabel = get_string('preview', 'quiz');
|
2008-11-20 06:59:11 +00:00
|
|
|
}
|
2009-02-11 10:08:07 +00:00
|
|
|
|
|
|
|
// Build the icon.
|
2011-02-10 20:44:47 +00:00
|
|
|
$strpreviewquestion = get_string('previewquestion', 'quiz');
|
2010-02-11 14:59:00 +00:00
|
|
|
$image = $OUTPUT->pix_icon('t/preview', $strpreviewquestion);
|
2009-08-20 13:17:07 +00:00
|
|
|
|
2011-05-27 11:49:23 +01:00
|
|
|
$action = new popup_action('click', $url, 'questionpreview',
|
|
|
|
question_preview_popup_params());
|
2009-12-30 15:19:55 +00:00
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
return $OUTPUT->action_link($url, $image, $action, array('title' => $strpreviewquestion));
|
2005-07-02 16:07:05 +00:00
|
|
|
}
|
2005-07-02 11:42:42 +00:00
|
|
|
|
2008-08-29 10:08:27 +00:00
|
|
|
/**
|
|
|
|
* @param object $attempt the attempt.
|
|
|
|
* @param object $context the quiz context.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return int whether flags should be shown/editable to the current user for this attempt.
|
2008-08-29 10:08:27 +00:00
|
|
|
*/
|
|
|
|
function quiz_get_flag_option($attempt, $context) {
|
|
|
|
global $USER;
|
2011-02-03 18:06:10 +00:00
|
|
|
if (!has_capability('moodle/question:flag', $context)) {
|
|
|
|
return question_display_options::HIDDEN;
|
|
|
|
} else if ($attempt->userid == $USER->id) {
|
|
|
|
return question_display_options::EDITABLE;
|
|
|
|
} else {
|
|
|
|
return question_display_options::VISIBLE;
|
2008-08-29 10:08:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2006-02-28 09:26:00 +00:00
|
|
|
/**
|
2012-04-24 15:01:12 +01:00
|
|
|
* Work out what state this quiz attempt is in - in the sense used by
|
|
|
|
* quiz_get_review_options, not in the sense of $attempt->state.
|
2011-02-03 18:06:10 +00:00
|
|
|
* @param object $quiz the quiz settings
|
|
|
|
* @param object $attempt the quiz_attempt database row.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @return int one of the mod_quiz_display_options::DURING,
|
2011-02-03 18:06:10 +00:00
|
|
|
* IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
|
2006-03-22 17:22:36 +00:00
|
|
|
*/
|
2011-02-03 18:06:10 +00:00
|
|
|
function quiz_attempt_state($quiz, $attempt) {
|
2012-10-02 15:51:45 +01:00
|
|
|
if ($attempt->state == quiz_attempt::IN_PROGRESS) {
|
2011-02-03 18:06:10 +00:00
|
|
|
return mod_quiz_display_options::DURING;
|
|
|
|
} else if (time() < $attempt->timefinish + 120) {
|
|
|
|
return mod_quiz_display_options::IMMEDIATELY_AFTER;
|
|
|
|
} else if (!$quiz->timeclose || time() < $quiz->timeclose) {
|
|
|
|
return mod_quiz_display_options::LATER_WHILE_OPEN;
|
|
|
|
} else {
|
|
|
|
return mod_quiz_display_options::AFTER_CLOSE;
|
|
|
|
}
|
2006-02-28 09:26:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-02-03 18:06:10 +00:00
|
|
|
* The the appropraite mod_quiz_display_options object for this attempt at this
|
|
|
|
* quiz right now.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
2006-08-25 16:03:54 +00:00
|
|
|
* @param object $quiz the quiz instance.
|
|
|
|
* @param object $attempt the attempt in question.
|
2011-02-03 18:06:10 +00:00
|
|
|
* @param $context the quiz context.
|
2006-12-22 12:52:13 +00:00
|
|
|
*
|
2011-02-03 18:06:10 +00:00
|
|
|
* @return mod_quiz_display_options
|
2006-08-25 16:03:54 +00:00
|
|
|
*/
|
2011-02-09 19:07:57 +00:00
|
|
|
function quiz_get_review_options($quiz, $attempt, $context) {
|
2011-02-03 18:06:10 +00:00
|
|
|
$options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt));
|
2008-08-29 10:08:27 +00:00
|
|
|
|
2006-02-28 09:26:00 +00:00
|
|
|
$options->readonly = true;
|
2008-08-29 10:08:27 +00:00
|
|
|
$options->flags = quiz_get_flag_option($attempt, $context);
|
2011-02-11 18:47:08 +00:00
|
|
|
if (!empty($attempt->id)) {
|
|
|
|
$options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php',
|
|
|
|
array('attempt' => $attempt->id));
|
|
|
|
}
|
2006-04-07 16:00:29 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Show a link to the comment box only for closed attempts.
|
2012-04-24 15:01:12 +01:00
|
|
|
if (!empty($attempt->id) && $attempt->state == quiz_attempt::FINISHED && !$attempt->preview &&
|
2011-02-11 18:47:08 +00:00
|
|
|
!is_null($context) && has_capability('mod/quiz:grade', $context)) {
|
2011-02-03 18:06:10 +00:00
|
|
|
$options->manualcomment = question_display_options::VISIBLE;
|
2011-02-09 19:07:57 +00:00
|
|
|
$options->manualcommentlink = new moodle_url('/mod/quiz/comment.php',
|
|
|
|
array('attempt' => $attempt->id));
|
2007-10-04 15:57:09 +00:00
|
|
|
}
|
|
|
|
|
2011-05-12 00:30:25 +01:00
|
|
|
if (!is_null($context) && !$attempt->preview &&
|
|
|
|
has_capability('mod/quiz:viewreports', $context) &&
|
2011-02-03 18:06:10 +00:00
|
|
|
has_capability('moodle/grade:viewhidden', $context)) {
|
2007-10-09 15:19:07 +00:00
|
|
|
// People who can see reports and hidden grades should be shown everything,
|
|
|
|
// except during preview when teachers want to see what students see.
|
2011-02-03 18:06:10 +00:00
|
|
|
$options->attempt = question_display_options::VISIBLE;
|
|
|
|
$options->correctness = question_display_options::VISIBLE;
|
|
|
|
$options->marks = question_display_options::MARK_AND_MAX;
|
|
|
|
$options->feedback = question_display_options::VISIBLE;
|
|
|
|
$options->numpartscorrect = question_display_options::VISIBLE;
|
|
|
|
$options->generalfeedback = question_display_options::VISIBLE;
|
|
|
|
$options->rightanswer = question_display_options::VISIBLE;
|
|
|
|
$options->overallfeedback = question_display_options::VISIBLE;
|
|
|
|
$options->history = question_display_options::VISIBLE;
|
2007-10-09 15:19:07 +00:00
|
|
|
|
2006-02-28 09:26:00 +00:00
|
|
|
}
|
2006-12-22 12:52:13 +00:00
|
|
|
|
2006-02-28 09:26:00 +00:00
|
|
|
return $options;
|
|
|
|
}
|
2006-12-22 12:52:13 +00:00
|
|
|
|
2006-04-18 22:55:28 +00:00
|
|
|
/**
|
2006-12-22 12:52:13 +00:00
|
|
|
* Combines the review options from a number of different quiz attempts.
|
2011-02-03 18:06:10 +00:00
|
|
|
* Returns an array of two ojects, so the suggested way of calling this
|
2006-12-22 12:52:13 +00:00
|
|
|
* 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.
|
|
|
|
*/
|
2011-02-03 18:06:10 +00:00
|
|
|
function quiz_get_combined_reviewoptions($quiz, $attempts) {
|
2011-03-16 14:34:19 +00:00
|
|
|
$fields = array('feedback', 'generalfeedback', 'rightanswer', 'overallfeedback');
|
2011-02-21 18:10:19 +00:00
|
|
|
$someoptions = new stdClass();
|
|
|
|
$alloptions = new stdClass();
|
2006-12-22 12:52:13 +00:00
|
|
|
foreach ($fields as $field) {
|
|
|
|
$someoptions->$field = false;
|
|
|
|
$alloptions->$field = true;
|
2006-04-18 22:55:28 +00:00
|
|
|
}
|
2011-03-16 14:34:19 +00:00
|
|
|
$someoptions->marks = question_display_options::HIDDEN;
|
|
|
|
$alloptions->marks = question_display_options::MARK_AND_MAX;
|
|
|
|
|
2006-12-22 12:52:13 +00:00
|
|
|
foreach ($attempts as $attempt) {
|
2011-02-03 18:06:10 +00:00
|
|
|
$attemptoptions = mod_quiz_display_options::make_from_quiz($quiz,
|
|
|
|
quiz_attempt_state($quiz, $attempt));
|
2007-04-05 13:54:20 +00:00
|
|
|
foreach ($fields as $field) {
|
|
|
|
$someoptions->$field = $someoptions->$field || $attemptoptions->$field;
|
|
|
|
$alloptions->$field = $alloptions->$field && $attemptoptions->$field;
|
|
|
|
}
|
2011-03-16 14:34:19 +00:00
|
|
|
$someoptions->marks = max($someoptions->marks, $attemptoptions->marks);
|
|
|
|
$alloptions->marks = min($alloptions->marks, $attemptoptions->marks);
|
2006-12-22 12:52:13 +00:00
|
|
|
}
|
|
|
|
return array($someoptions, $alloptions);
|
2006-04-18 22:55:28 +00:00
|
|
|
}
|
2007-06-26 16:34:05 +00:00
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param bool $removeemptypages If true, remove empty pages from the quiz. False by default.
|
2011-02-03 18:06:10 +00:00
|
|
|
* @return $string the cleaned-up layout
|
|
|
|
*/
|
|
|
|
function quiz_clean_layout($layout, $removeemptypages = false) {
|
|
|
|
// Remove repeated ','s. This can happen when a restore fails to find the right
|
|
|
|
// id to relink to.
|
|
|
|
$layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Remove duplicate question ids.
|
2011-02-03 18:06:10 +00:00
|
|
|
$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) {
|
2012-05-04 12:40:21 +01:00
|
|
|
// Avoid duplicate page breaks.
|
2011-02-03 18:06:10 +00:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Add a page break at the end if there is none.
|
2011-02-03 18:06:10 +00:00
|
|
|
if (end($cleanerlayout) !== '0') {
|
|
|
|
$cleanerlayout[] = '0';
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode(',', $cleanerlayout);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the slot for a question with a particular id.
|
|
|
|
* @param object $quiz the quiz settings.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $questionid the of a question in the quiz.
|
|
|
|
* @return int the corresponding slot. Null if the question is not in the quiz.
|
2011-02-03 18:06:10 +00:00
|
|
|
*/
|
|
|
|
function quiz_get_slot_for_question($quiz, $questionid) {
|
|
|
|
$questionids = quiz_questions_in_quiz($quiz->questions);
|
|
|
|
foreach (explode(',', $questionids) as $key => $id) {
|
|
|
|
if ($id == $questionid) {
|
|
|
|
return $key + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Functions for sending notification messages /////////////////////////////////
|
2007-06-26 16:34:05 +00:00
|
|
|
|
|
|
|
/**
|
2011-07-04 18:58:34 +01:00
|
|
|
* Sends a confirmation message to the student confirming that the attempt was processed.
|
2007-06-26 16:34:05 +00:00
|
|
|
*
|
2011-07-04 18:58:34 +01:00
|
|
|
* @param object $a lots of useful information that can be used in the message
|
|
|
|
* subject and body.
|
2007-06-26 16:34:05 +00:00
|
|
|
*
|
2011-07-04 18:58:34 +01:00
|
|
|
* @return int|false as for {@link message_send()}.
|
2007-06-26 16:34:05 +00:00
|
|
|
*/
|
2011-07-04 18:58:34 +01:00
|
|
|
function quiz_send_confirmation($recipient, $a) {
|
2007-06-26 16:34:05 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Add information about the recipient to $a.
|
2011-07-04 18:58:34 +01:00
|
|
|
// Don't do idnumber. we want idnumber to be the submitter's idnumber.
|
|
|
|
$a->username = fullname($recipient);
|
|
|
|
$a->userusername = $recipient->username;
|
2007-06-26 16:34:05 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Prepare the message.
|
2010-09-21 08:37:36 +00:00
|
|
|
$eventdata = new stdClass();
|
2011-07-04 18:58:34 +01:00
|
|
|
$eventdata->component = 'mod_quiz';
|
|
|
|
$eventdata->name = 'confirmation';
|
2010-11-19 09:50:12 +00:00
|
|
|
$eventdata->notification = 1;
|
|
|
|
|
2013-11-04 16:29:55 +08:00
|
|
|
$eventdata->userfrom = core_user::get_noreply_user();
|
2011-07-04 18:58:34 +01:00
|
|
|
$eventdata->userto = $recipient;
|
|
|
|
$eventdata->subject = get_string('emailconfirmsubject', 'quiz', $a);
|
|
|
|
$eventdata->fullmessage = get_string('emailconfirmbody', 'quiz', $a);
|
2008-07-24 08:38:03 +00:00
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN;
|
|
|
|
$eventdata->fullmessagehtml = '';
|
2010-11-19 09:50:12 +00:00
|
|
|
|
|
|
|
$eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a);
|
|
|
|
$eventdata->contexturl = $a->quizurl;
|
|
|
|
$eventdata->contexturlname = $a->quizname;
|
2011-01-23 18:34:41 +01:00
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
// ... and send it.
|
|
|
|
return message_send($eventdata);
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2010-11-19 05:18:34 +00:00
|
|
|
* Sends notification messages to the interested parties that assign the role capability
|
2007-06-26 16:34:05 +00:00
|
|
|
*
|
|
|
|
* @param object $recipient user object of the intended recipient
|
2011-02-21 18:10:19 +00:00
|
|
|
* @param object $a associative array of replaceable fields for the templates
|
2007-06-26 16:34:05 +00:00
|
|
|
*
|
2011-07-04 18:58:34 +01:00
|
|
|
* @return int|false as for {@link message_send()}.
|
2007-06-26 16:34:05 +00:00
|
|
|
*/
|
2011-07-04 18:58:34 +01:00
|
|
|
function quiz_send_notification($recipient, $submitter, $a) {
|
2007-06-26 16:34:05 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Recipient info for template.
|
2011-07-04 18:58:34 +01:00
|
|
|
$a->useridnumber = $recipient->idnumber;
|
|
|
|
$a->username = fullname($recipient);
|
2007-06-26 16:34:05 +00:00
|
|
|
$a->userusername = $recipient->username;
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Prepare the message.
|
2010-09-21 08:37:36 +00:00
|
|
|
$eventdata = new stdClass();
|
2011-07-04 18:58:34 +01:00
|
|
|
$eventdata->component = 'mod_quiz';
|
|
|
|
$eventdata->name = 'submission';
|
2010-11-19 05:18:34 +00:00
|
|
|
$eventdata->notification = 1;
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
$eventdata->userfrom = $submitter;
|
2008-07-24 08:38:03 +00:00
|
|
|
$eventdata->userto = $recipient;
|
2011-07-04 18:58:34 +01:00
|
|
|
$eventdata->subject = get_string('emailnotifysubject', 'quiz', $a);
|
|
|
|
$eventdata->fullmessage = get_string('emailnotifybody', 'quiz', $a);
|
2008-07-24 08:38:03 +00:00
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN;
|
|
|
|
$eventdata->fullmessagehtml = '';
|
2010-11-19 05:18:34 +00:00
|
|
|
|
|
|
|
$eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a);
|
|
|
|
$eventdata->contexturl = $a->quizreviewurl;
|
|
|
|
$eventdata->contexturlname = $a->quizname;
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
// ... and send it.
|
|
|
|
return message_send($eventdata);
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-07-04 18:58:34 +01:00
|
|
|
* Send all the requried messages when a quiz attempt is submitted.
|
2007-06-26 16:34:05 +00:00
|
|
|
*
|
|
|
|
* @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
|
|
|
|
*
|
2011-07-04 18:58:34 +01:00
|
|
|
* @return bool true if all necessary messages were sent successfully, else false.
|
2007-06-26 16:34:05 +00:00
|
|
|
*/
|
2011-07-04 18:58:34 +01:00
|
|
|
function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm) {
|
|
|
|
global $CFG, $DB;
|
2007-06-26 16:34:05 +00:00
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Do nothing if required objects not present.
|
2007-06-26 16:34:05 +00:00
|
|
|
if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
|
2011-07-04 18:58:34 +01:00
|
|
|
throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.');
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
$submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST);
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Check for confirmation required.
|
2007-06-26 16:34:05 +00:00
|
|
|
$sendconfirm = false;
|
|
|
|
$notifyexcludeusers = '';
|
2011-07-04 18:58:34 +01:00
|
|
|
if (has_capability('mod/quiz:emailconfirmsubmission', $context, $submitter, false)) {
|
|
|
|
$notifyexcludeusers = $submitter->id;
|
2007-06-26 16:34:05 +00:00
|
|
|
$sendconfirm = true;
|
|
|
|
}
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Check for notifications required.
|
2013-10-21 16:05:28 +08:00
|
|
|
$notifyfields = 'u.id, u.username, u.idnumber, u.email, u.emailstop, u.lang, u.timezone, u.mailformat, u.maildisplay, ';
|
|
|
|
$notifyfields .= get_all_user_name_fields(true, 'u');
|
2011-07-04 18:58:34 +01:00
|
|
|
$groups = groups_get_all_groups($course->id, $submitter->id);
|
2007-12-12 17:47:35 +00:00
|
|
|
if (is_array($groups) && count($groups) > 0) {
|
|
|
|
$groups = array_keys($groups);
|
2008-02-28 22:44:12 +00:00
|
|
|
} 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,
|
2011-07-04 18:58:34 +01:00
|
|
|
// then set $groups to a non-existant id so that only users with
|
2008-02-28 22:44:12 +00:00
|
|
|
// 'moodle/site:accessallgroups' get notified.
|
|
|
|
$groups = -1;
|
2007-12-12 17:47:35 +00:00
|
|
|
} else {
|
|
|
|
$groups = '';
|
|
|
|
}
|
2007-06-26 16:34:05 +00:00
|
|
|
$userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission',
|
2007-12-12 17:47:35 +00:00
|
|
|
$notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true);
|
2007-06-26 16:34:05 +00:00
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
if (empty($userstonotify) && !$sendconfirm) {
|
|
|
|
return true; // Nothing to do.
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
$a = new stdClass();
|
2012-05-04 12:40:21 +01:00
|
|
|
// Course info.
|
2011-07-04 18:58:34 +01:00
|
|
|
$a->coursename = $course->fullname;
|
|
|
|
$a->courseshortname = $course->shortname;
|
2012-05-04 12:40:21 +01:00
|
|
|
// Quiz info.
|
2011-07-04 18:58:34 +01:00
|
|
|
$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->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
|
|
|
|
$a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>';
|
2012-05-04 12:40:21 +01:00
|
|
|
// Attempt info.
|
2011-07-04 18:58:34 +01:00
|
|
|
$a->submissiontime = userdate($attempt->timefinish);
|
|
|
|
$a->timetaken = format_time($attempt->timefinish - $attempt->timestart);
|
2012-04-20 16:24:33 +01:00
|
|
|
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
|
|
|
|
$a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' .
|
|
|
|
format_string($quiz->name) . ' review</a>';
|
2012-05-04 12:40:21 +01:00
|
|
|
// Student who sat the quiz info.
|
2011-07-04 18:58:34 +01:00
|
|
|
$a->studentidnumber = $submitter->idnumber;
|
|
|
|
$a->studentname = fullname($submitter);
|
|
|
|
$a->studentusername = $submitter->username;
|
|
|
|
|
|
|
|
$allok = true;
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Send notifications if required.
|
2007-06-26 16:34:05 +00:00
|
|
|
if (!empty($userstonotify)) {
|
|
|
|
foreach ($userstonotify as $recipient) {
|
2011-07-04 18:58:34 +01:00
|
|
|
$allok = $allok && quiz_send_notification($recipient, $submitter, $a);
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
// Send confirmation if required. We send the student confirmation last, so
|
|
|
|
// that if message sending is being intermittently buggy, which means we send
|
|
|
|
// some but not all messages, and then try again later, then teachers may get
|
|
|
|
// duplicate messages, but the student will always get exactly one.
|
|
|
|
if ($sendconfirm) {
|
|
|
|
$allok = $allok && quiz_send_confirmation($submitter, $a);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $allok;
|
|
|
|
}
|
|
|
|
|
2012-04-20 16:24:33 +01:00
|
|
|
/**
|
|
|
|
* Send the notification message when a quiz attempt becomes overdue.
|
|
|
|
*
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
function quiz_send_overdue_message($course, $quiz, $attempt, $context, $cm) {
|
|
|
|
global $CFG, $DB;
|
|
|
|
|
2012-05-04 12:40:21 +01:00
|
|
|
// Do nothing if required objects not present.
|
2012-04-20 16:24:33 +01:00
|
|
|
if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) {
|
|
|
|
throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.');
|
|
|
|
}
|
|
|
|
|
|
|
|
$submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST);
|
|
|
|
|
|
|
|
if (!has_capability('mod/quiz:emailwarnoverdue', $context, $submitter, false)) {
|
|
|
|
return; // Message not required.
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare lots of useful information that admins might want to include in
|
|
|
|
// the email message.
|
|
|
|
$quizname = format_string($quiz->name);
|
|
|
|
|
|
|
|
$deadlines = array();
|
|
|
|
if ($quiz->timelimit) {
|
|
|
|
$deadlines[] = $attempt->timestart + $quiz->timelimit;
|
|
|
|
}
|
|
|
|
if ($quiz->timeclose) {
|
|
|
|
$deadlines[] = $quiz->timeclose;
|
|
|
|
}
|
2012-04-30 13:46:02 +01:00
|
|
|
$duedate = min($deadlines);
|
|
|
|
$graceend = $duedate + $quiz->graceperiod;
|
2012-04-20 16:24:33 +01:00
|
|
|
|
|
|
|
$a = new stdClass();
|
|
|
|
// Course info.
|
|
|
|
$a->coursename = $course->fullname;
|
|
|
|
$a->courseshortname = $course->shortname;
|
|
|
|
// Quiz info.
|
|
|
|
$a->quizname = $quizname;
|
|
|
|
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
|
|
|
|
$a->quizlink = '<a href="' . $a->quizurl . '">' . $quizname . '</a>';
|
|
|
|
// Attempt info.
|
2012-04-30 13:46:02 +01:00
|
|
|
$a->attemptduedate = userdate($duedate);
|
|
|
|
$a->attemptgraceend = userdate($graceend);
|
2012-04-20 16:24:33 +01:00
|
|
|
$a->attemptsummaryurl = $CFG->wwwroot . '/mod/quiz/summary.php?attempt=' . $attempt->id;
|
|
|
|
$a->attemptsummarylink = '<a href="' . $a->attemptsummaryurl . '">' . $quizname . ' review</a>';
|
|
|
|
// Student's info.
|
|
|
|
$a->studentidnumber = $submitter->idnumber;
|
|
|
|
$a->studentname = fullname($submitter);
|
|
|
|
$a->studentusername = $submitter->username;
|
|
|
|
|
|
|
|
// Prepare the message.
|
|
|
|
$eventdata = new stdClass();
|
|
|
|
$eventdata->component = 'mod_quiz';
|
|
|
|
$eventdata->name = 'attempt_overdue';
|
|
|
|
$eventdata->notification = 1;
|
|
|
|
|
2013-11-04 16:29:55 +08:00
|
|
|
$eventdata->userfrom = core_user::get_noreply_user();
|
2012-04-20 16:24:33 +01:00
|
|
|
$eventdata->userto = $submitter;
|
|
|
|
$eventdata->subject = get_string('emailoverduesubject', 'quiz', $a);
|
|
|
|
$eventdata->fullmessage = get_string('emailoverduebody', 'quiz', $a);
|
|
|
|
$eventdata->fullmessageformat = FORMAT_PLAIN;
|
|
|
|
$eventdata->fullmessagehtml = '';
|
|
|
|
|
|
|
|
$eventdata->smallmessage = get_string('emailoverduesmall', 'quiz', $a);
|
|
|
|
$eventdata->contexturl = $a->quizurl;
|
|
|
|
$eventdata->contexturlname = $a->quizname;
|
|
|
|
|
|
|
|
// Send the message.
|
|
|
|
return message_send($eventdata);
|
|
|
|
}
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
/**
|
|
|
|
* Handle the quiz_attempt_submitted event.
|
|
|
|
*
|
|
|
|
* This sends the confirmation and notification messages, if required.
|
|
|
|
*
|
|
|
|
* @param object $event the event object.
|
|
|
|
*/
|
|
|
|
function quiz_attempt_submitted_handler($event) {
|
|
|
|
global $DB;
|
|
|
|
|
|
|
|
$course = $DB->get_record('course', array('id' => $event->courseid));
|
2013-09-03 13:38:44 +08:00
|
|
|
$attempt = $event->get_record_snapshot('quiz_attempts', $event->objectid);
|
|
|
|
$quiz = $event->get_record_snapshot('quiz', $attempt->quiz);
|
|
|
|
$cm = get_coursemodule_from_id('quiz', $event->get_context()->instanceid, $event->courseid);
|
2011-07-04 18:58:34 +01:00
|
|
|
|
|
|
|
if (!($course && $quiz && $cm && $attempt)) {
|
|
|
|
// Something has been deleted since the event was raised. Therefore, the
|
|
|
|
// event is no longer relevant.
|
|
|
|
return true;
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
|
|
|
|
2011-07-04 18:58:34 +01:00
|
|
|
return quiz_send_notification_messages($course, $quiz, $attempt,
|
2012-07-24 16:55:49 +08:00
|
|
|
context_module::instance($cm->id), $cm);
|
2007-06-26 16:34:05 +00:00
|
|
|
}
|
2008-11-20 06:59:11 +00:00
|
|
|
|
2012-04-20 16:24:33 +01:00
|
|
|
/**
|
|
|
|
* Handle the quiz_attempt_overdue event.
|
|
|
|
*
|
|
|
|
* For quizzes with applicable settings, this sends a message to the user, reminding
|
|
|
|
* them that they forgot to submit, and that they have another chance to do so.
|
|
|
|
*
|
|
|
|
* @param object $event the event object.
|
|
|
|
*/
|
|
|
|
function quiz_attempt_overdue_handler($event) {
|
|
|
|
global $DB;
|
|
|
|
|
|
|
|
$course = $DB->get_record('course', array('id' => $event->courseid));
|
2013-09-03 13:38:44 +08:00
|
|
|
$attempt = $event->get_record_snapshot('quiz_attempts', $event->objectid);
|
|
|
|
$quiz = $event->get_record_snapshot('quiz', $attempt->quiz);
|
|
|
|
$cm = get_coursemodule_from_id('quiz', $event->get_context()->instanceid, $event->courseid);
|
2012-04-20 16:24:33 +01:00
|
|
|
|
|
|
|
if (!($course && $quiz && $cm && $attempt)) {
|
|
|
|
// Something has been deleted since the event was raised. Therefore, the
|
|
|
|
// event is no longer relevant.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return quiz_send_overdue_message($course, $quiz, $attempt,
|
2012-07-24 16:55:49 +08:00
|
|
|
context_module::instance($cm->id), $cm);
|
2012-04-20 16:24:33 +01:00
|
|
|
}
|
|
|
|
|
2012-10-09 13:48:30 -04:00
|
|
|
/**
|
|
|
|
* Handle groups_member_added event
|
|
|
|
*
|
|
|
|
* @param object $event the event object.
|
2013-09-19 18:36:00 +08:00
|
|
|
* @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_added()}.
|
2012-10-09 13:48:30 -04:00
|
|
|
*/
|
|
|
|
function quiz_groups_member_added_handler($event) {
|
2013-09-19 18:36:00 +08:00
|
|
|
debugging('quiz_groups_member_added_handler() is deprecated, please use ' .
|
|
|
|
'\mod_quiz\group_observers::group_member_added() instead.', DEBUG_DEVELOPER);
|
2012-10-09 13:48:30 -04:00
|
|
|
quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle groups_member_removed event
|
|
|
|
*
|
|
|
|
* @param object $event the event object.
|
2013-09-19 18:36:00 +08:00
|
|
|
* @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_removed()}.
|
2012-10-09 13:48:30 -04:00
|
|
|
*/
|
|
|
|
function quiz_groups_member_removed_handler($event) {
|
2013-09-19 18:36:00 +08:00
|
|
|
debugging('quiz_groups_member_removed_handler() is deprecated, please use ' .
|
|
|
|
'\mod_quiz\group_observers::group_member_removed() instead.', DEBUG_DEVELOPER);
|
2012-10-09 13:48:30 -04:00
|
|
|
quiz_update_open_attempts(array('userid'=>$event->userid, 'groupid'=>$event->groupid));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle groups_group_deleted event
|
|
|
|
*
|
|
|
|
* @param object $event the event object.
|
2013-09-19 18:36:00 +08:00
|
|
|
* @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_deleted()}.
|
2012-10-09 13:48:30 -04:00
|
|
|
*/
|
|
|
|
function quiz_groups_group_deleted_handler($event) {
|
|
|
|
global $DB;
|
2013-09-19 18:36:00 +08:00
|
|
|
debugging('quiz_groups_group_deleted_handler() is deprecated, please use ' .
|
|
|
|
'\mod_quiz\group_observers::group_deleted() instead.', DEBUG_DEVELOPER);
|
|
|
|
quiz_process_group_deleted_in_course($event->courseid);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logic to happen when a/some group(s) has/have been deleted in a course.
|
|
|
|
*
|
|
|
|
* @param int $courseid The course ID.
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
function quiz_process_group_deleted_in_course($courseid) {
|
|
|
|
global $DB;
|
2012-10-09 13:48:30 -04:00
|
|
|
|
|
|
|
// It would be nice if we got the groupid that was deleted.
|
2013-09-19 18:36:00 +08:00
|
|
|
// Instead, we just update all quizzes with orphaned group overrides.
|
2012-10-09 13:48:30 -04:00
|
|
|
$sql = "SELECT o.id, o.quiz
|
|
|
|
FROM {quiz_overrides} o
|
|
|
|
JOIN {quiz} quiz ON quiz.id = o.quiz
|
|
|
|
LEFT JOIN {groups} grp ON grp.id = o.groupid
|
|
|
|
WHERE quiz.course = :courseid AND grp.id IS NULL";
|
2013-09-19 18:36:00 +08:00
|
|
|
$params = array('courseid' => $courseid);
|
2012-10-09 13:48:30 -04:00
|
|
|
$records = $DB->get_records_sql_menu($sql, $params);
|
2012-12-11 09:02:07 +00:00
|
|
|
if (!$records) {
|
|
|
|
return; // Nothing to do.
|
|
|
|
}
|
2012-10-09 13:48:30 -04:00
|
|
|
$DB->delete_records_list('quiz_overrides', 'id', array_keys($records));
|
2013-09-19 18:36:00 +08:00
|
|
|
quiz_update_open_attempts(array('quizid' => array_unique(array_values($records))));
|
2012-10-09 13:48:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle groups_members_removed event
|
|
|
|
*
|
|
|
|
* @param object $event the event object.
|
2013-09-19 18:36:00 +08:00
|
|
|
* @deprecated since 2.6, see {@link \mod_quiz\group_observers::group_member_removed()}.
|
2012-10-09 13:48:30 -04:00
|
|
|
*/
|
|
|
|
function quiz_groups_members_removed_handler($event) {
|
2013-09-19 18:36:00 +08:00
|
|
|
debugging('quiz_groups_members_removed_handler() is deprecated, please use ' .
|
|
|
|
'\mod_quiz\group_observers::group_member_removed() instead.', DEBUG_DEVELOPER);
|
2012-10-09 13:48:30 -04:00
|
|
|
if ($event->userid == 0) {
|
|
|
|
quiz_update_open_attempts(array('courseid'=>$event->courseid));
|
|
|
|
} else {
|
|
|
|
quiz_update_open_attempts(array('courseid'=>$event->courseid, 'userid'=>$event->userid));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-04-20 16:24:33 +01:00
|
|
|
/**
|
|
|
|
* Get the information about the standard quiz JavaScript module.
|
|
|
|
* @return array a standard jsmodule structure.
|
|
|
|
*/
|
2010-08-04 15:22:04 +00:00
|
|
|
function quiz_get_js_module() {
|
2010-11-01 17:44:59 +00:00
|
|
|
global $PAGE;
|
2011-09-14 20:25:31 +01:00
|
|
|
|
2010-08-04 15:22:04 +00:00
|
|
|
return array(
|
|
|
|
'name' => 'mod_quiz',
|
|
|
|
'fullpath' => '/mod/quiz/module.js',
|
2011-05-12 00:30:25 +01:00
|
|
|
'requires' => array('base', 'dom', 'event-delegate', 'event-key',
|
2012-09-27 14:13:24 +01:00
|
|
|
'core_question_engine', 'moodle-core-formchangechecker'),
|
2010-08-04 15:22:04 +00:00
|
|
|
'strings' => array(
|
2011-09-14 20:25:31 +01:00
|
|
|
array('cancel', 'moodle'),
|
2010-08-09 17:10:25 +00:00
|
|
|
array('flagged', 'question'),
|
2011-10-05 20:58:38 +01:00
|
|
|
array('functiondisabledbysecuremode', 'quiz'),
|
|
|
|
array('startattempt', 'quiz'),
|
|
|
|
array('timesup', 'quiz'),
|
2012-09-27 14:13:24 +01:00
|
|
|
array('changesmadereallygoaway', 'moodle'),
|
2010-08-04 15:22:04 +00:00
|
|
|
),
|
|
|
|
);
|
2010-08-10 09:56:48 +00:00
|
|
|
}
|
2011-02-03 18:06:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An extension of question_display_options that includes the extra options used
|
|
|
|
* by the quiz.
|
|
|
|
*
|
2011-02-23 16:25:25 +00:00
|
|
|
* @copyright 2010 The Open University
|
|
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
2011-02-03 18:06:10 +00:00
|
|
|
*/
|
|
|
|
class mod_quiz_display_options extends question_display_options {
|
|
|
|
/**#@+
|
|
|
|
* @var integer bits used to indicate various times in relation to a
|
|
|
|
* quiz attempt.
|
|
|
|
*/
|
|
|
|
const DURING = 0x10000;
|
|
|
|
const IMMEDIATELY_AFTER = 0x01000;
|
|
|
|
const LATER_WHILE_OPEN = 0x00100;
|
|
|
|
const AFTER_CLOSE = 0x00010;
|
|
|
|
/**#@-*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var boolean if this is false, then the student is not allowed to review
|
|
|
|
* anything about the attempt.
|
|
|
|
*/
|
|
|
|
public $attempt = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var boolean if this is false, then the student is not allowed to review
|
|
|
|
* anything about the attempt.
|
|
|
|
*/
|
|
|
|
public $overallfeedback = self::VISIBLE;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up the various options from the quiz settings, and a time constant.
|
2011-02-21 18:10:19 +00:00
|
|
|
* @param object $quiz the quiz settings.
|
2011-02-23 16:25:25 +00:00
|
|
|
* @param int $one of the {@link DURING}, {@link IMMEDIATELY_AFTER},
|
2011-02-03 18:06:10 +00:00
|
|
|
* {@link LATER_WHILE_OPEN} or {@link AFTER_CLOSE} constants.
|
|
|
|
* @return mod_quiz_display_options set up appropriately.
|
|
|
|
*/
|
|
|
|
public static function make_from_quiz($quiz, $when) {
|
|
|
|
$options = new self();
|
|
|
|
|
|
|
|
$options->attempt = self::extract($quiz->reviewattempt, $when, true, false);
|
|
|
|
$options->correctness = self::extract($quiz->reviewcorrectness, $when);
|
2011-05-12 00:30:25 +01:00
|
|
|
$options->marks = self::extract($quiz->reviewmarks, $when,
|
|
|
|
self::MARK_AND_MAX, self::MAX_ONLY);
|
2011-02-03 18:06:10 +00:00
|
|
|
$options->feedback = self::extract($quiz->reviewspecificfeedback, $when);
|
|
|
|
$options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when);
|
|
|
|
$options->rightanswer = self::extract($quiz->reviewrightanswer, $when);
|
|
|
|
$options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when);
|
|
|
|
|
|
|
|
$options->numpartscorrect = $options->feedback;
|
|
|
|
|
|
|
|
if ($quiz->questiondecimalpoints != -1) {
|
|
|
|
$options->markdp = $quiz->questiondecimalpoints;
|
|
|
|
} else {
|
|
|
|
$options->markdp = $quiz->decimalpoints;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $options;
|
|
|
|
}
|
|
|
|
|
2011-05-12 00:30:25 +01:00
|
|
|
protected static function extract($bitmask, $bit,
|
|
|
|
$whenset = self::VISIBLE, $whennotset = self::HIDDEN) {
|
2011-02-03 18:06:10 +00:00
|
|
|
if ($bitmask & $bit) {
|
|
|
|
return $whenset;
|
|
|
|
} else {
|
|
|
|
return $whennotset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A {@link qubaid_condition} for finding all the question usages belonging to
|
|
|
|
* a particular quiz.
|
|
|
|
*
|
2011-02-23 16:25:25 +00:00
|
|
|
* @copyright 2010 The Open University
|
|
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
2011-02-03 18:06:10 +00:00
|
|
|
*/
|
2011-03-16 18:56:14 +00:00
|
|
|
class qubaids_for_quiz extends qubaid_join {
|
2011-02-03 18:06:10 +00:00
|
|
|
public function __construct($quizid, $includepreviews = true, $onlyfinished = false) {
|
2011-03-16 18:56:14 +00:00
|
|
|
$where = 'quiza.quiz = :quizaquiz';
|
2012-04-24 15:01:12 +01:00
|
|
|
$params = array('quizaquiz' => $quizid);
|
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
if (!$includepreviews) {
|
|
|
|
$where .= ' AND preview = 0';
|
|
|
|
}
|
2012-04-24 15:01:12 +01:00
|
|
|
|
2011-02-03 18:06:10 +00:00
|
|
|
if ($onlyfinished) {
|
2012-04-24 15:01:12 +01:00
|
|
|
$where .= ' AND state == :statefinished';
|
|
|
|
$params['statefinished'] = quiz_attempt::FINISHED;
|
2011-02-03 18:06:10 +00:00
|
|
|
}
|
|
|
|
|
2012-04-24 15:01:12 +01:00
|
|
|
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
|
2011-02-03 18:06:10 +00:00
|
|
|
}
|
|
|
|
}
|