Merge branch 'MDL-76897' of https://github.com/timhunt/moodle
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
namespace core_backup;
|
||||
|
||||
use mod_quiz\quiz_attempt;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -381,7 +382,7 @@ class restore_stepslib_date_test extends \restore_date_testcase {
|
||||
// Make a user to do the quiz.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
|
||||
$quizobj = \mod_quiz\quiz_settings::create($quiz->id, $user1->id);
|
||||
$quizobj = quiz_settings::create($quiz->id, $user1->id);
|
||||
|
||||
// Start the attempt.
|
||||
$quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace core_grades;
|
||||
|
||||
use grade_plugin_return;
|
||||
use grade_report_grader;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -478,8 +479,7 @@ class report_graderlib_test extends \advanced_testcase {
|
||||
|
||||
// Set the grade for the second one to 0 (note, you have to do this after creating it,
|
||||
// otherwise it doesn't create an ungraded grade item).
|
||||
$ungradedquiz->instance = $ungradedquiz->id;
|
||||
quiz_set_grade(0, $ungradedquiz);
|
||||
quiz_settings::create($ungradedquiz->id)->get_grade_calculator()->update_quiz_maximum_grade(0);
|
||||
|
||||
// Set current user.
|
||||
$this->setUser($manager);
|
||||
|
||||
@@ -29,6 +29,7 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
require_once($CFG->dirroot . '/question/editlib.php');
|
||||
|
||||
use mod_quiz\form\add_random_form;
|
||||
use mod_quiz\quiz_settings;
|
||||
use qbank_managecategories\question_category_object;
|
||||
|
||||
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
|
||||
@@ -41,10 +42,9 @@ $addonpage = optional_param('addonpage', 0, PARAM_INT);
|
||||
$category = optional_param('category', 0, PARAM_INT);
|
||||
$mdlscrollto = optional_param('mdlscrollto', 0, PARAM_INT);
|
||||
|
||||
// Get the course object and related bits.
|
||||
if (!$course = $DB->get_record('course', ['id' => $quiz->course])) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
$quizobj = quiz_settings::create($quiz->id);
|
||||
$course = $quizobj->get_course();
|
||||
|
||||
// You need mod/quiz:manage in addition to question capabilities to access this page.
|
||||
// You also need the moodle/question:useall capability somewhere.
|
||||
require_capability('mod/quiz:manage', $contexts->lowest());
|
||||
@@ -113,7 +113,7 @@ if ($data = $mform->get_data()) {
|
||||
|
||||
quiz_add_random_questions($quiz, $addonpage, $categoryid, $data->numbertoadd, $includesubcategories, $tagids);
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
|
||||
redirect($returnurl);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class custom_completion extends activity_custom_completion {
|
||||
}
|
||||
$lastfinishedattempt = end($attempts);
|
||||
$context = context_module::instance($this->cm->id);
|
||||
$quizobj = quiz_settings::create($this->cm->instance, $this->userid);
|
||||
$quizobj = quiz_settings::create((int) $this->cm->instance, $this->userid);
|
||||
$accessmanager = new access_manager(
|
||||
$quizobj,
|
||||
time(),
|
||||
|
||||
+2
-2
@@ -64,14 +64,14 @@ class submit_question_version extends external_api {
|
||||
'newversion' => $newversion
|
||||
];
|
||||
$params = self::validate_parameters(self::execute_parameters(), $params);
|
||||
$response = ['result' => false];
|
||||
$response = [];
|
||||
// Get the required data.
|
||||
$referencedata = $DB->get_record('question_references',
|
||||
['itemid' => $params['slotid'], 'component' => 'mod_quiz', 'questionarea' => 'slot']);
|
||||
$slotdata = $DB->get_record('quiz_slots', ['id' => $slotid]);
|
||||
|
||||
// Capability check.
|
||||
list($course, $cm) = get_course_and_cm_from_instance($slotdata->quizid, 'quiz');
|
||||
[, $cm] = get_course_and_cm_from_instance($slotdata->quizid, 'quiz');
|
||||
$context = \context_module::instance($cm->id);
|
||||
self::validate_context($context);
|
||||
require_capability('mod/quiz:manage', $context);
|
||||
|
||||
@@ -60,14 +60,14 @@ class edit_override_form extends moodleform {
|
||||
* Constructor.
|
||||
*
|
||||
* @param moodle_url $submiturl the form action URL.
|
||||
* @param cm_info $cm course module object.
|
||||
* @param cm_info|stdClass $cm course module object.
|
||||
* @param stdClass $quiz the quiz settings object.
|
||||
* @param context_module $context the quiz context.
|
||||
* @param bool $groupmode editing group override (true) or user override (false).
|
||||
* @param stdClass|null $override the override being edited, if it already exists.
|
||||
*/
|
||||
public function __construct(moodle_url $submiturl,
|
||||
cm_info $cm, stdClass $quiz, context_module $context,
|
||||
cm_info|stdClass $cm, stdClass $quiz, context_module $context,
|
||||
bool $groupmode, ?stdClass $override) {
|
||||
|
||||
$this->cm = $cm;
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
namespace mod_quiz;
|
||||
|
||||
use coding_exception;
|
||||
use mod_quiz\event\quiz_grade_updated;
|
||||
use question_engine_data_mapper;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* This class contains all the logic for computing the grade of a quiz.
|
||||
*
|
||||
* There are two sorts of calculation which need to be done. For a single
|
||||
* attempt, we need to compute the total attempt score from score for each question.
|
||||
* And for a quiz user, we need to compute the final grade from all the separate attempt grades.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class grade_calculator {
|
||||
|
||||
/** @var float a number that is effectively zero. Used to avoid division-by-zero or underflow problems. */
|
||||
const ALMOST_ZERO = 0.000005;
|
||||
|
||||
/** @var quiz_settings the quiz for which this instance computes grades. */
|
||||
protected $quizobj;
|
||||
|
||||
/**
|
||||
* Constructor. Recommended way to get an instance is $quizobj->get_grade_calculator();
|
||||
*
|
||||
* @param quiz_settings $quizobj
|
||||
*/
|
||||
protected function __construct(quiz_settings $quizobj) {
|
||||
$this->quizobj = $quizobj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory. The recommended way to get an instance is $quizobj->get_grade_calculator();
|
||||
*
|
||||
* @param quiz_settings $quizobj settings of a quiz.
|
||||
* @return grade_calculator instance of this class for the given quiz.
|
||||
*/
|
||||
public static function create(quiz_settings $quizobj): grade_calculator {
|
||||
return new self($quizobj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* You should call {@see quiz_delete_previews()} before you call this function.
|
||||
*/
|
||||
public function recompute_quiz_sumgrades(): void {
|
||||
global $DB;
|
||||
$quiz = $this->quizobj->get_quiz();
|
||||
|
||||
// Update sumgrades in the database.
|
||||
$DB->execute("
|
||||
UPDATE {quiz}
|
||||
SET sumgrades = COALESCE((
|
||||
SELECT SUM(maxmark)
|
||||
FROM {quiz_slots}
|
||||
WHERE quizid = {quiz}.id
|
||||
), 0)
|
||||
WHERE id = ?
|
||||
", [$quiz->id]);
|
||||
|
||||
// Update the value in memory.
|
||||
$quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', ['id' => $quiz->id]);
|
||||
|
||||
if ($quiz->sumgrades < self::ALMOST_ZERO && 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.
|
||||
self::update_quiz_maximum_grade(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sumgrades field of attempts at this quiz.
|
||||
*/
|
||||
public function recompute_all_attempt_sumgrades(): void {
|
||||
global $DB;
|
||||
$dm = new question_engine_data_mapper();
|
||||
$timenow = time();
|
||||
|
||||
$DB->execute("
|
||||
UPDATE {quiz_attempts}
|
||||
SET timemodified = :timenow,
|
||||
sumgrades = (
|
||||
{$dm->sum_usage_marks_subquery('uniqueid')}
|
||||
)
|
||||
WHERE quiz = :quizid AND state = :finishedstate
|
||||
", [
|
||||
'timenow' => $timenow,
|
||||
'quizid' => $this->quizobj->get_quizid(),
|
||||
'finishedstate' => quiz_attempt::FINISHED
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the final grade at this quiz for a particular student.
|
||||
*
|
||||
* That is, given the quiz settings, and all the attempts this user has made,
|
||||
* compute their final grade for the quiz, as shown in the gradebook.
|
||||
*
|
||||
* The $attempts parameter is for efficiency. If you already have the data for
|
||||
* all this user's attempts loaded (for example from {@see quiz_get_user_attempts()}
|
||||
* or because you are looping through a large recordset fetched in one efficient query,
|
||||
* then you can pass that data here to save DB queries.
|
||||
*
|
||||
* @param int|null $userid The userid to calculate the grade for. Defaults to the current user.
|
||||
* @param array $attempts if you already have this user's attempt records loaded, pass them here to save queries.
|
||||
*/
|
||||
public function recompute_final_grade(?int $userid = null, array $attempts = []): void {
|
||||
global $DB, $USER;
|
||||
$quiz = $this->quizobj->get_quiz();
|
||||
|
||||
if (empty($userid)) {
|
||||
$userid = $USER->id;
|
||||
}
|
||||
|
||||
if (!$attempts) {
|
||||
// Get all the attempts made by the user.
|
||||
$attempts = quiz_get_user_attempts($quiz->id, $userid);
|
||||
}
|
||||
|
||||
// Calculate the best grade.
|
||||
$bestgrade = $this->compute_final_grade_from_attempts($attempts);
|
||||
$bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
|
||||
|
||||
// Save the best grade in the database.
|
||||
if (is_null($bestgrade)) {
|
||||
$DB->delete_records('quiz_grades', ['quiz' => $quiz->id, 'userid' => $userid]);
|
||||
|
||||
} else if ($grade = $DB->get_record('quiz_grades',
|
||||
['quiz' => $quiz->id, 'userid' => $userid])) {
|
||||
$grade->grade = $bestgrade;
|
||||
$grade->timemodified = time();
|
||||
$DB->update_record('quiz_grades', $grade);
|
||||
|
||||
} else {
|
||||
$grade = new stdClass();
|
||||
$grade->quiz = $quiz->id;
|
||||
$grade->userid = $userid;
|
||||
$grade->grade = $bestgrade;
|
||||
$grade->timemodified = time();
|
||||
$DB->insert_record('quiz_grades', $grade);
|
||||
}
|
||||
|
||||
quiz_update_grades($quiz, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the overall grade for a quiz given a number of attempts by a particular user.
|
||||
*
|
||||
* @param array $attempts an array of all the user's attempts at this quiz in order.
|
||||
* @return float|null the overall grade, or null if the user does not have a grade.
|
||||
*/
|
||||
protected function compute_final_grade_from_attempts(array $attempts): ?float {
|
||||
|
||||
$grademethod = $this->quizobj->get_quiz()->grademethod;
|
||||
switch ($grademethod) {
|
||||
|
||||
case QUIZ_ATTEMPTFIRST:
|
||||
$firstattempt = reset($attempts);
|
||||
return $firstattempt->sumgrades;
|
||||
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
$lastattempt = end($attempts);
|
||||
return $lastattempt->sumgrades;
|
||||
|
||||
case QUIZ_GRADEAVERAGE:
|
||||
$sum = 0;
|
||||
$count = 0;
|
||||
foreach ($attempts as $attempt) {
|
||||
if (!is_null($attempt->sumgrades)) {
|
||||
$sum += $attempt->sumgrades;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
if ($count == 0) {
|
||||
return null;
|
||||
}
|
||||
return $sum / $count;
|
||||
|
||||
case QUIZ_GRADEHIGHEST:
|
||||
$max = null;
|
||||
foreach ($attempts as $attempt) {
|
||||
if ($attempt->sumgrades > $max) {
|
||||
$max = $attempt->sumgrades;
|
||||
}
|
||||
}
|
||||
return $max;
|
||||
|
||||
default:
|
||||
throw new coding_exception('Unrecognised grading method ' . $grademethod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the final grade at this quiz for all students.
|
||||
*
|
||||
* This function is equivalent to calling {@see recompute_final_grade()} for all
|
||||
* users who have attempted the quiz, but is much more efficient.
|
||||
*/
|
||||
public function recompute_all_final_grades(): void {
|
||||
global $DB;
|
||||
$quiz = $this->quizobj->get_quiz();
|
||||
|
||||
// If the quiz does not contain any graded questions, then there is nothing to do.
|
||||
if (!$quiz->sumgrades) {
|
||||
return;
|
||||
}
|
||||
|
||||
$param = ['iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED];
|
||||
$firstlastattemptjoin = "JOIN (
|
||||
SELECT
|
||||
iquiza.userid,
|
||||
MIN(attempt) AS firstattempt,
|
||||
MAX(attempt) AS lastattempt
|
||||
|
||||
FROM {quiz_attempts} iquiza
|
||||
|
||||
WHERE
|
||||
iquiza.state = :istatefinished AND
|
||||
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:
|
||||
// Because of the where clause, there will only be one row, but we
|
||||
// must still use an aggregate function.
|
||||
$select = 'MAX(quiza.sumgrades)';
|
||||
$join = $firstlastattemptjoin;
|
||||
$where = 'quiza.attempt = first_last_attempts.firstattempt AND';
|
||||
break;
|
||||
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
// Because of the where clause, there will only be one row, but we
|
||||
// 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 >= self::ALMOST_ZERO) {
|
||||
$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;
|
||||
$param['statefinished'] = quiz_attempt::FINISHED;
|
||||
$param['statefinished2'] = quiz_attempt::FINISHED;
|
||||
$param['almostzero'] = self::ALMOST_ZERO;
|
||||
$finalgradesubquery = "
|
||||
SELECT quiza.userid, $finalgrade AS newgrade
|
||||
FROM {quiz_attempts} quiza
|
||||
$join
|
||||
WHERE
|
||||
$where
|
||||
quiza.state = :statefinished AND
|
||||
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
|
||||
quiza2.state = :statefinished2 AND
|
||||
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) > :almostzero OR
|
||||
((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).
|
||||
$param);
|
||||
|
||||
$timenow = time();
|
||||
$todelete = [];
|
||||
foreach ($changedgrades as $changedgrade) {
|
||||
|
||||
if (is_null($changedgrade->newgrade)) {
|
||||
$todelete[] = $changedgrade->userid;
|
||||
|
||||
} else if (is_null($changedgrade->grade)) {
|
||||
$toinsert = new stdClass();
|
||||
$toinsert->quiz = $quiz->id;
|
||||
$toinsert->userid = $changedgrade->userid;
|
||||
$toinsert->timemodified = $timenow;
|
||||
$toinsert->grade = $changedgrade->newgrade;
|
||||
$DB->insert_record('quiz_grades', $toinsert);
|
||||
|
||||
} else {
|
||||
$toupdate = new stdClass();
|
||||
$toupdate->id = $changedgrade->id;
|
||||
$toupdate->grade = $changedgrade->newgrade;
|
||||
$toupdate->timemodified = $timenow;
|
||||
$DB->update_record('quiz_grades', $toupdate);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($todelete)) {
|
||||
list($test, $params) = $DB->get_in_or_equal($todelete);
|
||||
$DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test,
|
||||
array_merge([$quiz->id], $params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the quiz setting for the grade the quiz is out of.
|
||||
*
|
||||
* This function will update the data in quiz_grades and quiz_feedback, and
|
||||
* pass the new grades on to the gradebook.
|
||||
*
|
||||
* @param float $newgrade the new maximum grade for the quiz.
|
||||
*/
|
||||
public function update_quiz_maximum_grade(float $newgrade): void {
|
||||
global $DB;
|
||||
$quiz = $this->quizobj->get_quiz();
|
||||
|
||||
// This is potentially expensive, so only do it if necessary.
|
||||
if (abs($quiz->grade - $newgrade) < self::ALMOST_ZERO) {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a transaction.
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
// Update the quiz table.
|
||||
$oldgrade = $quiz->grade;
|
||||
$quiz->grade = $newgrade;
|
||||
$timemodified = time();
|
||||
$DB->update_record('quiz', (object) [
|
||||
'id' => $quiz->id,
|
||||
'grade' => $newgrade,
|
||||
'timemodified' => $timemodified,
|
||||
]);
|
||||
|
||||
// Rescale the grade of all quiz attempts.
|
||||
if ($oldgrade < $newgrade) {
|
||||
// The new total is bigger, so we need to recompute fully to avoid underflow problems.
|
||||
$this->recompute_all_final_grades();
|
||||
|
||||
} else {
|
||||
// New total smaller, so we can rescale the grades efficiently.
|
||||
$DB->execute("
|
||||
UPDATE {quiz_grades}
|
||||
SET grade = ? * grade, timemodified = ?
|
||||
WHERE quiz = ?
|
||||
", [$newgrade / $oldgrade, $timemodified, $quiz->id]);
|
||||
}
|
||||
|
||||
// Rescale the overall feedback boundaries.
|
||||
if ($oldgrade > self::ALMOST_ZERO) {
|
||||
// Update the quiz_feedback table.
|
||||
$factor = $newgrade / $oldgrade;
|
||||
$DB->execute("
|
||||
UPDATE {quiz_feedback}
|
||||
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
|
||||
WHERE quizid = ?
|
||||
", [$factor, $factor, $quiz->id]);
|
||||
}
|
||||
|
||||
// Update grade item and send all grades to gradebook.
|
||||
quiz_grade_item_update($quiz);
|
||||
quiz_update_grades($quiz);
|
||||
|
||||
// Log quiz grade updated event.
|
||||
quiz_grade_updated::create([
|
||||
'context' => $this->quizobj->get_context(),
|
||||
'objectid' => $quiz->id,
|
||||
'other' => [
|
||||
'oldgrade' => $oldgrade + 0, // Remove trailing 0s.
|
||||
'newgrade' => $newgrade,
|
||||
]
|
||||
])->trigger();
|
||||
|
||||
$transaction->allow_commit();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ namespace mod_quiz\local\reports;
|
||||
|
||||
use coding_exception;
|
||||
use context_module;
|
||||
use mod_quiz\quiz_settings;
|
||||
use moodle_url;
|
||||
use stdClass;
|
||||
use table_sql;
|
||||
@@ -63,6 +64,9 @@ abstract class attempts_report extends report_base {
|
||||
/** @var boolean caches the results of {@see should_show_grades()}. */
|
||||
protected $showgrades = null;
|
||||
|
||||
/** @var quiz_settings|null quiz settings object. Set in the init method. */
|
||||
protected $quizobj = null;
|
||||
|
||||
/**
|
||||
* Can be used in subclasses to cache this information, but it will only get set if you set it.
|
||||
* @example an example use in quiz_overview_report.
|
||||
@@ -88,8 +92,8 @@ abstract class attempts_report extends report_base {
|
||||
*/
|
||||
public function init($mode, $formclass, $quiz, $cm, $course): array {
|
||||
$this->mode = $mode;
|
||||
|
||||
$this->context = context_module::instance($cm->id);
|
||||
$this->quizobj = new quiz_settings($quiz, $cm, $course);
|
||||
$this->context = $this->quizobj->get_context();
|
||||
|
||||
[$currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins] = $this->get_students_joins(
|
||||
$cm, $course);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace mod_quiz\local\structure;
|
||||
|
||||
use context_module;
|
||||
|
||||
/**
|
||||
* Class slot_random, represents a random question slot type.
|
||||
*
|
||||
@@ -210,7 +212,7 @@ class slot_random {
|
||||
// Log slot created event.
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
|
||||
$event = \mod_quiz\event\slot_created::create([
|
||||
'context' => \context_module::instance($cm->id),
|
||||
'context' => context_module::instance($cm->id),
|
||||
'objectid' => $this->record->id,
|
||||
'other' => [
|
||||
'quizid' => $quiz->id,
|
||||
|
||||
@@ -751,7 +751,7 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
|
||||
if ($structure->can_display_number_be_customised($slot)) {
|
||||
$questionnumbercustomised = $this->output->render($structure->make_slot_display_number_in_place_editable(
|
||||
$slotid, \context_module::instance($structure->get_cmid())));
|
||||
$slotid, $structure->get_context()));
|
||||
$questionnumber = $questionnumbercustomised;
|
||||
} else {
|
||||
$questionnumber = $structure->get_displayed_number_for_slot($slot);
|
||||
|
||||
@@ -1032,7 +1032,7 @@ class renderer extends plugin_renderer_base {
|
||||
* Output the page information
|
||||
*
|
||||
* @param stdClass $quiz the quiz settings.
|
||||
* @param stdClass $cm the course_module object.
|
||||
* @param cm_info|stdClass $cm the course_module object.
|
||||
* @param context $context the quiz context.
|
||||
* @param array $messages any access messages that should be described.
|
||||
* @param bool $quizhasquestions does quiz has questions added.
|
||||
@@ -1357,11 +1357,11 @@ class renderer extends plugin_renderer_base {
|
||||
* Render a summary of the number of group and user overrides, with corresponding links.
|
||||
*
|
||||
* @param stdClass $quiz the quiz settings.
|
||||
* @param stdClass $cm the cm object.
|
||||
* @param cm_info|stdClass $cm the cm object.
|
||||
* @param int $currentgroup currently selected group, if there is one.
|
||||
* @return string HTML fragment for the link.
|
||||
*/
|
||||
public function quiz_override_summary_links(stdClass $quiz, stdClass $cm, $currentgroup = 0): string {
|
||||
public function quiz_override_summary_links(stdClass $quiz, cm_info|stdClass $cm, $currentgroup = 0): string {
|
||||
|
||||
$baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]);
|
||||
$counts = quiz_override_summary($quiz, $cm, $currentgroup);
|
||||
|
||||
@@ -125,7 +125,7 @@ class quiz_attempt {
|
||||
|
||||
$attempt = $DB->get_record('quiz_attempts', $conditions, '*', MUST_EXIST);
|
||||
$quiz = access_manager::load_quiz_and_settings($attempt->quiz);
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$course = get_course($quiz->course);
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
|
||||
|
||||
// Update quiz with override information.
|
||||
@@ -363,6 +363,15 @@ class quiz_attempt {
|
||||
return $this->quizobj->get_cmid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quiz context.
|
||||
*
|
||||
* @return context_module the context of the quiz this attempt belongs to.
|
||||
*/
|
||||
public function get_context(): context_module {
|
||||
return $this->quizobj->get_context();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the current user is someone who previews the quiz, rather than attempting it?
|
||||
*
|
||||
@@ -1634,7 +1643,7 @@ class quiz_attempt {
|
||||
}
|
||||
|
||||
if (!$this->is_preview() && $this->attempt->state == self::FINISHED) {
|
||||
quiz_save_best_grade($this->get_quiz(), $this->get_userid());
|
||||
$this->recompute_final_grade();
|
||||
}
|
||||
|
||||
$transaction->allow_commit();
|
||||
@@ -1761,7 +1770,7 @@ class quiz_attempt {
|
||||
$DB->update_record('quiz_attempts', $this->attempt);
|
||||
|
||||
if (!$this->is_preview()) {
|
||||
quiz_save_best_grade($this->get_quiz(), $this->attempt->userid);
|
||||
$this->recompute_final_grade();
|
||||
|
||||
// Trigger event.
|
||||
$this->fire_state_transition_event('\mod_quiz\event\attempt_submitted', $timestamp, $studentisonline);
|
||||
@@ -1786,6 +1795,13 @@ class quiz_attempt {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be called after this attempt's grade is changed, to update the overall quiz grade.
|
||||
*/
|
||||
protected function recompute_final_grade(): void {
|
||||
$this->quizobj->get_grade_calculator()->recompute_final_grade($this->get_userid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this attempt as now overdue.
|
||||
*
|
||||
@@ -2108,7 +2124,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid(),
|
||||
'page' => $this->get_currentpage()
|
||||
@@ -2129,7 +2145,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid(),
|
||||
'page' => $this->get_currentpage()
|
||||
@@ -2150,7 +2166,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid(),
|
||||
'page' => $this->get_currentpage()
|
||||
@@ -2173,7 +2189,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid(),
|
||||
'page' => $this->get_currentpage(),
|
||||
@@ -2197,7 +2213,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid()
|
||||
]
|
||||
@@ -2218,7 +2234,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid()
|
||||
]
|
||||
@@ -2236,7 +2252,7 @@ class quiz_attempt {
|
||||
'objectid' => $this->get_attemptid(),
|
||||
'relateduserid' => $this->get_userid(),
|
||||
'courseid' => $this->get_courseid(),
|
||||
'context' => context_module::instance($this->get_cmid()),
|
||||
'context' => $this->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $this->get_quizid()
|
||||
]
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace mod_quiz;
|
||||
|
||||
use cm_info;
|
||||
use coding_exception;
|
||||
use context;
|
||||
use context_module;
|
||||
@@ -84,19 +85,15 @@ class quiz_settings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to create a new quiz object for a specific user.
|
||||
* Helper used by the other factory methods.
|
||||
*
|
||||
* @param int $quizid the the quiz id.
|
||||
* @param stdClass $quiz
|
||||
* @param cm_info|stdClass $cm
|
||||
* @param stdClass $course
|
||||
* @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
|
||||
* @return quiz_settings the new quiz object.
|
||||
* @return quiz_settings the new quiz settings object.
|
||||
*/
|
||||
public static function create($quizid, $userid = null) {
|
||||
global $DB;
|
||||
|
||||
$quiz = access_manager::load_quiz_and_settings($quizid);
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
|
||||
|
||||
protected static function create_helper(stdClass $quiz, cm_info|stdClass $cm, stdClass $course, ?int $userid): self {
|
||||
// Update quiz with override information.
|
||||
if ($userid) {
|
||||
$quiz = quiz_update_effective_access($quiz, $userid);
|
||||
@@ -105,6 +102,35 @@ class quiz_settings {
|
||||
return new quiz_settings($quiz, $cm, $course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to create a new quiz settings object from a quiz id, for a specific user.
|
||||
*
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
|
||||
* @return quiz_settings the new quiz settings object.
|
||||
*/
|
||||
public static function create(int $quizid, int $userid = null): self {
|
||||
$quiz = access_manager::load_quiz_and_settings($quizid);
|
||||
$course = get_course($quiz->course);
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
|
||||
|
||||
return self::create_helper($quiz, $cm, $course, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to create a new quiz settings object from a cmid, for a specific user.
|
||||
*
|
||||
* @param int $cmid the course-module id.
|
||||
* @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
|
||||
* @return quiz_settings the new quiz settings object.
|
||||
*/
|
||||
public static function create_for_cmid(int $cmid, int $userid = null): self {
|
||||
[$course, $cm] = get_course_and_cm_from_cmid($cmid, 'quiz');
|
||||
$quiz = access_manager::load_quiz_and_settings($cm->instance);
|
||||
|
||||
return self::create_helper($quiz, $cm, $course, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@see quiz_attempt} for an attempt at this quiz.
|
||||
*
|
||||
@@ -347,7 +373,7 @@ class quiz_settings {
|
||||
* for this quiz at this time.
|
||||
*
|
||||
* @param int $timenow the current time as a unix timestamp.
|
||||
* @return access_manager and instance of the access_manager class
|
||||
* @return access_manager an instance of the access_manager class
|
||||
* for this quiz at this time.
|
||||
*/
|
||||
public function get_access_manager($timenow) {
|
||||
@@ -358,6 +384,15 @@ class quiz_settings {
|
||||
return $this->accessmanager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the grade_calculator object for this quiz.
|
||||
*
|
||||
* @return grade_calculator
|
||||
*/
|
||||
public function get_grade_calculator(): grade_calculator {
|
||||
return grade_calculator::create($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper round the has_capability funciton that automatically passes in the quiz context.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace mod_quiz;
|
||||
|
||||
use context_module;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
use mod_quiz\question\qubaids_for_quiz;
|
||||
use stdClass;
|
||||
@@ -341,6 +342,15 @@ class structure {
|
||||
return $this->quizobj->get_cmid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quiz context.
|
||||
*
|
||||
* @return context_module the context of the quiz that this is the structure of.
|
||||
*/
|
||||
public function get_context(): context_module {
|
||||
return $this->quizobj->get_context();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id of the quiz.
|
||||
*
|
||||
|
||||
@@ -97,7 +97,7 @@ class quiz_notify_attempt_manual_grading_completed extends \core\task\scheduled_
|
||||
}
|
||||
|
||||
if (!$course || $course->id != $quiz->course) {
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$course = get_course($quiz->course);
|
||||
$coursecontext = context_course::instance($quiz->course);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class update_overdue_attempts extends \core\task\scheduled_task {
|
||||
|
||||
// If we have moved on to a different course, fetch the new data.
|
||||
if (!$course || $course->id != $quiz->course) {
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$course = get_course($quiz->course);
|
||||
}
|
||||
|
||||
// Make a specialised version of the quiz settings, with the relevant overrides.
|
||||
|
||||
@@ -90,7 +90,7 @@ if (data_submitted() && confirm_sesskey()) {
|
||||
$params = [
|
||||
'objectid' => $attemptobj->get_question_attempt($slot)->get_question_id(),
|
||||
'courseid' => $attemptobj->get_courseid(),
|
||||
'context' => context_module::instance($attemptobj->get_cmid()),
|
||||
'context' => $attemptobj->get_quizobj()->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $attemptobj->get_quizid(),
|
||||
'attemptid' => $attemptobj->get_attemptid(),
|
||||
|
||||
@@ -305,3 +305,182 @@ class moodle_quiz_exception extends moodle_exception {
|
||||
parent::__construct($errorcode, 'quiz', $link, $a, $debuginfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* You should call {@see quiz_delete_previews()} before you call this function.
|
||||
*
|
||||
* @param stdClass $quiz a quiz.
|
||||
* @deprecated since Moodle 4.2. Please use grade_calculator::recompute_quiz_sumgrades.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_update_sumgrades($quiz) {
|
||||
debugging('quiz_update_sumgrades is deprecated. ' .
|
||||
'Please use a standard grade_calculator::recompute_quiz_sumgrades instead.', DEBUG_DEVELOPER);
|
||||
quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_quiz_sumgrades();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sumgrades field of the attempts at a quiz.
|
||||
*
|
||||
* @param stdClass $quiz a quiz.
|
||||
* @deprecated since Moodle 4.2. Please use grade_calculator::recompute_all_attempt_sumgrades.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_update_all_attempt_sumgrades($quiz) {
|
||||
debugging('quiz_update_all_attempt_sumgrades is deprecated. ' .
|
||||
'Please use a standard grade_calculator::recompute_all_attempt_sumgrades instead.', DEBUG_DEVELOPER);
|
||||
quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_all_attempt_sumgrades();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 stdClass $quiz the quiz settings.
|
||||
* @deprecated since Moodle 4.2. Please use grade_calculator::recompute_all_final_grades.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_update_all_final_grades($quiz) {
|
||||
debugging('quiz_update_all_final_grades is deprecated. ' .
|
||||
'Please use a standard grade_calculator::recompute_all_final_grades instead.', DEBUG_DEVELOPER);
|
||||
quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_all_final_grades();
|
||||
}
|
||||
|
||||
/**
|
||||
* The quiz grade is the maximum that student's results are marked out of. When it
|
||||
* changes, the corresponding data in quiz_grades and quiz_feedback needs to be
|
||||
* rescaled. After calling this function, you probably need to call
|
||||
* quiz_update_all_attempt_sumgrades, grade_calculator::recompute_all_final_grades();
|
||||
* quiz_update_grades. (At least, that is what this comment has said for years, but
|
||||
* it seems to call recompute_all_final_grades itself.)
|
||||
*
|
||||
* @param float $newgrade the new maximum grade for the quiz.
|
||||
* @param stdClass $quiz the quiz we are updating. Passed by reference so its
|
||||
* grade field can be updated too.
|
||||
* @return bool indicating success or failure.
|
||||
* @deprecated since Moodle 4.2. Please use grade_calculator::update_quiz_maximum_grade.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_set_grade($newgrade, $quiz) {
|
||||
debugging('quiz_set_grade is deprecated. ' .
|
||||
'Please use a standard grade_calculator::update_quiz_maximum_grade instead.', DEBUG_DEVELOPER);
|
||||
quiz_settings::create($quiz->id)->get_grade_calculator()->update_quiz_maximum_grade($newgrade);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the overall grade for a user at a quiz in the quiz_grades table
|
||||
*
|
||||
* @param stdClass $quiz The quiz for which the best grade is to be calculated and then saved.
|
||||
* @param int $userid The userid to calculate the grade for. Defaults to the current user.
|
||||
* @param array $attempts The attempts of this user. Useful if you are
|
||||
* looping through many users. Attempts can be fetched in one master query to
|
||||
* avoid repeated querying.
|
||||
* @return bool Indicates success or failure.
|
||||
* @deprecated since Moodle 4.2. Please use grade_calculator::update_quiz_maximum_grade.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_save_best_grade($quiz, $userid = null, $attempts = []) {
|
||||
debugging('quiz_save_best_grade is deprecated. ' .
|
||||
'Please use a standard grade_calculator::recompute_final_grade instead.', DEBUG_DEVELOPER);
|
||||
quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_final_grade($userid, $attempts);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the overall grade for a quiz given a number of attempts by a particular user.
|
||||
*
|
||||
* @param stdClass $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
|
||||
* @deprecated since Moodle 4.2. No direct replacement.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_calculate_best_grade($quiz, $attempts) {
|
||||
debugging('quiz_calculate_best_grade is deprecated with no direct replacement. It was only used ' .
|
||||
'in one place in the quiz code so this logic is now private to grade_calculator.', DEBUG_DEVELOPER);
|
||||
|
||||
switch ($quiz->grademethod) {
|
||||
|
||||
case QUIZ_ATTEMPTFIRST:
|
||||
$firstattempt = reset($attempts);
|
||||
return $firstattempt->sumgrades;
|
||||
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
$lastattempt = end($attempts);
|
||||
return $lastattempt->sumgrades;
|
||||
|
||||
case QUIZ_GRADEAVERAGE:
|
||||
$sum = 0;
|
||||
$count = 0;
|
||||
foreach ($attempts as $attempt) {
|
||||
if (!is_null($attempt->sumgrades)) {
|
||||
$sum += $attempt->sumgrades;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
if ($count == 0) {
|
||||
return null;
|
||||
}
|
||||
return $sum / $count;
|
||||
|
||||
case QUIZ_GRADEHIGHEST:
|
||||
default:
|
||||
$max = null;
|
||||
foreach ($attempts as $attempt) {
|
||||
if ($attempt->sumgrades > $max) {
|
||||
$max = $attempt->sumgrades;
|
||||
}
|
||||
}
|
||||
return $max;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt with the best grade for a quiz
|
||||
*
|
||||
* Which attempt is the best depends on $quiz->grademethod. If the grade
|
||||
* method is GRADEAVERAGE then this function simply returns the last attempt.
|
||||
* @return stdClass The attempt with the best grade
|
||||
* @param stdClass $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
|
||||
* @deprecated since Moodle 4.2. No direct replacement.
|
||||
* @todo MDL-76612 Final deprecation in Moodle 4.6
|
||||
*/
|
||||
function quiz_calculate_best_attempt($quiz, $attempts) {
|
||||
debugging('quiz_calculate_best_attempt is deprecated with no direct replacement. ' .
|
||||
'It was not used anywhere!', DEBUG_DEVELOPER);
|
||||
|
||||
switch ($quiz->grademethod) {
|
||||
|
||||
case QUIZ_ATTEMPTFIRST:
|
||||
foreach ($attempts as $attempt) {
|
||||
return $attempt;
|
||||
}
|
||||
break;
|
||||
|
||||
case QUIZ_GRADEAVERAGE: // We need to do something with it.
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
foreach ($attempts as $attempt) {
|
||||
$final = $attempt;
|
||||
}
|
||||
return $final;
|
||||
|
||||
default:
|
||||
case QUIZ_GRADEHIGHEST:
|
||||
$max = -1;
|
||||
foreach ($attempts as $attempt) {
|
||||
if ($attempt->sumgrades > $max) {
|
||||
$max = $attempt->sumgrades;
|
||||
$maxattempt = $attempt;
|
||||
}
|
||||
}
|
||||
return $maxattempt;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -51,22 +51,23 @@ $mdlscrollto = optional_param('mdlscrollto', '', PARAM_INT);
|
||||
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
|
||||
question_edit_setup('editq', '/mod/quiz/edit.php', true);
|
||||
|
||||
$PAGE->set_url($thispageurl);
|
||||
$PAGE->set_secondary_active_tab("mod_quiz_edit");
|
||||
|
||||
// You need mod/quiz:manage in addition to question capabilities to access this page.
|
||||
require_capability('mod/quiz:manage', $contexts->lowest());
|
||||
|
||||
// Get the course object and related bits.
|
||||
$course = get_course($quiz->course);
|
||||
$quizobj = new quiz_settings($quiz, $cm, $course);
|
||||
$structure = $quizobj->get_structure();
|
||||
$gradecalculator = $quizobj->get_grade_calculator();
|
||||
|
||||
$defaultcategoryobj = question_make_default_categories($contexts->all());
|
||||
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
|
||||
|
||||
$quizhasattempts = quiz_has_attempts($quiz->id);
|
||||
|
||||
$PAGE->set_url($thispageurl);
|
||||
$PAGE->set_secondary_active_tab("mod_quiz_edit");
|
||||
|
||||
// Get the course object and related bits.
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$quizobj = new quiz_settings($quiz, $cm, $course);
|
||||
$structure = $quizobj->get_structure();
|
||||
|
||||
// You need mod/quiz:manage in addition to question capabilities to access this page.
|
||||
require_capability('mod/quiz:manage', $contexts->lowest());
|
||||
|
||||
// Process commands ============================================================.
|
||||
|
||||
// Get the list of question ids had their check-boxes ticked.
|
||||
@@ -100,7 +101,7 @@ if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sess
|
||||
$addonpage = optional_param('addonpage', 0, PARAM_INT);
|
||||
quiz_add_quiz_question($addquestion, $quiz, $addonpage);
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$gradecalculator->recompute_quiz_sumgrades();
|
||||
$thispageurl->param('lastchanged', $addquestion);
|
||||
redirect($afteractionurl);
|
||||
}
|
||||
@@ -118,7 +119,7 @@ if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
|
||||
}
|
||||
}
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$gradecalculator->recompute_quiz_sumgrades();
|
||||
redirect($afteractionurl);
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@ if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) {
|
||||
quiz_add_random_questions($quiz, $addonpage, $categoryid, $randomcount, $recurse);
|
||||
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$gradecalculator->recompute_quiz_sumgrades();
|
||||
redirect($afteractionurl);
|
||||
}
|
||||
|
||||
@@ -149,9 +150,7 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
|
||||
// If rescaling is required save the new maximum.
|
||||
$maxgrade = unformat_float(optional_param('maxgrade', '', PARAM_RAW_TRIMMED), true);
|
||||
if (is_float($maxgrade) && $maxgrade >= 0) {
|
||||
quiz_set_grade($maxgrade, $quiz);
|
||||
quiz_update_all_final_grades($quiz);
|
||||
quiz_update_grades($quiz, 0, true);
|
||||
$gradecalculator->update_quiz_maximum_grade($maxgrade);
|
||||
}
|
||||
|
||||
redirect($afteractionurl);
|
||||
|
||||
+11
-10
@@ -54,14 +54,15 @@ $PAGE->set_url('/mod/quiz/edit-rest.php',
|
||||
['quizid' => $quizid, 'class' => $class]);
|
||||
|
||||
require_sesskey();
|
||||
$quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create($quizid);
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
require_login($course, false, $cm);
|
||||
|
||||
$quizobj = new quiz_settings($quiz, $cm, $course);
|
||||
$structure = $quizobj->get_structure();
|
||||
$modcontext = context_module::instance($cm->id);
|
||||
$gradecalculator = $quizobj->get_grade_calculator();
|
||||
$modcontext = $quizobj->get_context();
|
||||
|
||||
echo $OUTPUT->header(); // Send headers.
|
||||
|
||||
@@ -131,9 +132,9 @@ switch($requestmethod) {
|
||||
if ($structure->update_slot_maxmark($slot, $maxmark)) {
|
||||
// Grade has really changed.
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
quiz_update_all_attempt_sumgrades($quiz);
|
||||
quiz_update_all_final_grades($quiz);
|
||||
$gradecalculator->recompute_quiz_sumgrades();
|
||||
$gradecalculator->recompute_all_attempt_sumgrades();
|
||||
$gradecalculator->recompute_all_final_grades();
|
||||
quiz_update_grades($quiz, 0, true);
|
||||
}
|
||||
$result = ['instancemaxmark' => quiz_format_question_grade($quiz, $maxmark),
|
||||
@@ -163,7 +164,7 @@ switch($requestmethod) {
|
||||
}
|
||||
}
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$gradecalculator->recompute_quiz_sumgrades();
|
||||
|
||||
$result = ['newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades),
|
||||
'deleted' => true, 'newnumquestions' => $structure->get_question_count()];
|
||||
@@ -203,7 +204,7 @@ switch($requestmethod) {
|
||||
}
|
||||
$structure->remove_slot($slot->slot);
|
||||
quiz_delete_previews($quiz);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$gradecalculator->recompute_quiz_sumgrades();
|
||||
$result = ['newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades),
|
||||
'deleted' => true, 'newnumquestions' => $structure->get_question_count()];
|
||||
break;
|
||||
|
||||
+9
-12
@@ -23,6 +23,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
|
||||
@@ -30,18 +32,13 @@ $slotid = required_param('slotid', PARAM_INT);
|
||||
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
|
||||
|
||||
// Get the quiz slot.
|
||||
$slot = $DB->get_record('quiz_slots', ['id' => $slotid]);
|
||||
if (!$slot) {
|
||||
new moodle_exception('invalidrandomslot', 'mod_quiz');
|
||||
}
|
||||
$slot = $DB->get_record('quiz_slots', ['id' => $slotid], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create($slot->quizid);
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
|
||||
if (!$quiz = $DB->get_record('quiz', ['id' => $slot->quizid])) {
|
||||
new moodle_exception('invalidquizid', 'quiz');
|
||||
}
|
||||
|
||||
$cm = get_coursemodule_from_instance('quiz', $slot->quizid, $quiz->course);
|
||||
|
||||
require_login($cm->course, false, $cm);
|
||||
require_login($course, false, $cm);
|
||||
|
||||
if ($returnurl) {
|
||||
$returnurl = new moodle_url($returnurl);
|
||||
@@ -67,7 +64,7 @@ if (!$category = $DB->get_record('question_categories', ['id' => $filterconditio
|
||||
$catcontext = context::instance_by_id($category->contextid);
|
||||
require_capability('moodle/question:useall', $catcontext);
|
||||
|
||||
$thiscontext = context_module::instance($cm->id);
|
||||
$thiscontext = $quizobj->get_context();
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
|
||||
// Create the editing form.
|
||||
|
||||
+6
-4
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
|
||||
use mod_quiz\quiz_attempt;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
@@ -34,12 +35,13 @@ require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
|
||||
$id = required_param('id', PARAM_INT);
|
||||
$userid = optional_param('userid', 0, PARAM_INT);
|
||||
|
||||
$cm = get_coursemodule_from_id('quiz', $id, 0, false, MUST_EXIST);
|
||||
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
|
||||
$quiz = $DB->get_record('quiz', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create_for_cmid($id);
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
require_login($course, false, $cm);
|
||||
|
||||
$reportlist = quiz_report_list(context_module::instance($cm->id));
|
||||
$reportlist = quiz_report_list($quizobj->get_context());
|
||||
if (empty($reportlist) || $userid == $USER->id) {
|
||||
// If the user cannot see reports, or can see reports but is looking
|
||||
// at their own grades, redirect them to the view.php page.
|
||||
|
||||
+2
-3
@@ -27,10 +27,9 @@ require_once("../../config.php");
|
||||
require_once("locallib.php");
|
||||
|
||||
$id = required_param('id', PARAM_INT);
|
||||
|
||||
$PAGE->set_url('/mod/quiz/index.php', ['id' => $id]);
|
||||
if (!$course = $DB->get_record('course', ['id' => $id])) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
$course = get_course($id);
|
||||
$coursecontext = context_course::instance($id);
|
||||
require_login($course);
|
||||
$PAGE->set_pagelayout('incourse');
|
||||
|
||||
+10
-10
@@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use mod_quiz\access_manager;
|
||||
use mod_quiz\form\add_random_form;
|
||||
use mod_quiz\grade_calculator;
|
||||
use mod_quiz\question\bank\custom_view;
|
||||
use mod_quiz\question\display_options;
|
||||
use mod_quiz\question\qubaids_for_quiz;
|
||||
@@ -149,7 +150,8 @@ function quiz_update_instance($quiz, $mform) {
|
||||
quiz_after_add_or_update($quiz);
|
||||
|
||||
if ($oldquiz->grademethod != $quiz->grademethod) {
|
||||
quiz_update_all_final_grades($quiz);
|
||||
$gradecalculator = quiz_settings::create($quiz->id)->get_grade_calculator();
|
||||
$gradecalculator->recompute_all_final_grades();
|
||||
quiz_update_grades($quiz);
|
||||
}
|
||||
|
||||
@@ -461,7 +463,7 @@ function quiz_get_best_grade($quiz, $userid) {
|
||||
* @return bool whether this is a graded quiz.
|
||||
*/
|
||||
function quiz_has_grades($quiz) {
|
||||
return $quiz->grade >= 0.000005 && $quiz->sumgrades >= 0.000005;
|
||||
return $quiz->grade >= grade_calculator::ALMOST_ZERO && $quiz->sumgrades >= grade_calculator::ALMOST_ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1171,21 +1173,19 @@ function quiz_review_option_form_to_db($fromform, $field) {
|
||||
* @return \core\output\inplace_editable|void
|
||||
*/
|
||||
function mod_quiz_inplace_editable(string $itemtype, int $itemid, string $newvalue): \core\output\inplace_editable {
|
||||
global $DB;
|
||||
|
||||
if ($itemtype === 'slotdisplaynumber') {
|
||||
global $DB;
|
||||
$record = $DB->get_record('quiz_slots', ['id' => $itemid], '*', MUST_EXIST);
|
||||
$quiz = $DB->get_record('quiz', ['id' => $record->quizid], '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create($record->quizid);
|
||||
|
||||
// Call validate_context for course module to check access and set current context.
|
||||
$context = context_module::instance($cm->id);
|
||||
$context = $quizobj->get_context();
|
||||
\core_external\external_api::validate_context($context);
|
||||
|
||||
// Check permission of the user to update this item (customise question number).
|
||||
require_capability('mod/quiz:manage', $context);
|
||||
|
||||
$quizobj = new quiz_settings($quiz, $cm, $course);
|
||||
$structure = $quizobj->get_structure();
|
||||
$warning = false;
|
||||
// Clean input and update the record.
|
||||
@@ -2117,7 +2117,7 @@ function mod_quiz_core_calendar_provide_event_action(calendar_event $event,
|
||||
* when printing this activity in a course listing. See get_array_of_activities() in course/lib.php.
|
||||
*
|
||||
* @param stdClass $coursemodule The coursemodule object (record).
|
||||
* @return cached_cm_info An object on information that the courses
|
||||
* @return cached_cm_info|false An object on information that the courses
|
||||
* will know about (most noticeably, an icon).
|
||||
*/
|
||||
function quiz_get_coursemodule_info($coursemodule) {
|
||||
@@ -2424,7 +2424,7 @@ function mod_quiz_output_fragment_quiz_question_bank($args) {
|
||||
question_build_edit_resources('editq', '/mod/quiz/edit.php', $params, custom_view::DEFAULT_PAGE_SIZE);
|
||||
|
||||
// Get the course object and related bits.
|
||||
$course = $DB->get_record('course', ['id' => $quiz->course], '*', MUST_EXIST);
|
||||
$course = get_course($quiz->course);
|
||||
require_capability('mod/quiz:manage', $contexts->lowest());
|
||||
|
||||
// Create quiz question bank view.
|
||||
|
||||
+16
-420
@@ -36,6 +36,8 @@ require_once($CFG->libdir . '/filelib.php');
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
use mod_quiz\access_manager;
|
||||
use mod_quiz\event\attempt_submitted;
|
||||
use mod_quiz\grade_calculator;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
use mod_quiz\question\display_options;
|
||||
use mod_quiz\quiz_attempt;
|
||||
@@ -97,7 +99,7 @@ function quiz_create_attempt(quiz_settings $quizobj, $attemptnumber, $lastattemp
|
||||
}
|
||||
|
||||
$quiz = $quizobj->get_quiz();
|
||||
if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) {
|
||||
if ($quiz->sumgrades < grade_calculator::ALMOST_ZERO && $quiz->grade > grade_calculator::ALMOST_ZERO) {
|
||||
throw new moodle_exception('cannotstartgradesmismatch', 'quiz',
|
||||
new moodle_url('/mod/quiz/view.php', ['q' => $quiz->id]),
|
||||
['grade' => quiz_format_grade($quiz, $quiz->grade)]);
|
||||
@@ -442,10 +444,11 @@ function quiz_delete_attempt($attempt, $quiz) {
|
||||
// If none, then delete record for this quiz, this user from quiz_grades
|
||||
// else recalculate best grade.
|
||||
$userid = $attempt->userid;
|
||||
$gradecalculator = quiz_settings::create($quiz->id)->get_grade_calculator();
|
||||
if (!$DB->record_exists('quiz_attempts', ['userid' => $userid, 'quiz' => $quiz->id])) {
|
||||
$DB->delete_records('quiz_grades', ['userid' => $userid, 'quiz' => $quiz->id]);
|
||||
} else {
|
||||
quiz_save_best_grade($quiz, $userid);
|
||||
$gradecalculator->recompute_final_grade($userid);
|
||||
}
|
||||
|
||||
quiz_update_grades($quiz, $userid);
|
||||
@@ -530,6 +533,7 @@ function quiz_repaginate_questions($quizid, $slotsperpage) {
|
||||
}
|
||||
|
||||
// Functions to do with quiz grades ////////////////////////////////////////////
|
||||
// Note a lot of logic related to this is now in the grade_calculator class.
|
||||
|
||||
/**
|
||||
* Convert the raw grade stored in $attempt into a grade out of the maximum
|
||||
@@ -545,7 +549,7 @@ function quiz_repaginate_questions($quizid, $slotsperpage) {
|
||||
function quiz_rescale_grade($rawgrade, $quiz, $format = true) {
|
||||
if (is_null($rawgrade)) {
|
||||
$grade = null;
|
||||
} else if ($quiz->sumgrades >= 0.000005) {
|
||||
} else if ($quiz->sumgrades >= grade_calculator::ALMOST_ZERO) {
|
||||
$grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
|
||||
} else {
|
||||
$grade = 0;
|
||||
@@ -626,373 +630,6 @@ function quiz_has_feedback($quiz) {
|
||||
return $cache[$quiz->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* You should call {@link quiz_delete_previews()} before you call this function.
|
||||
*
|
||||
* @param stdClass $quiz a quiz.
|
||||
*/
|
||||
function quiz_update_sumgrades($quiz) {
|
||||
global $DB;
|
||||
|
||||
$sql = 'UPDATE {quiz}
|
||||
SET sumgrades = COALESCE((
|
||||
SELECT SUM(maxmark)
|
||||
FROM {quiz_slots}
|
||||
WHERE quizid = {quiz}.id
|
||||
), 0)
|
||||
WHERE id = ?';
|
||||
$DB->execute($sql, [$quiz->id]);
|
||||
$quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', ['id' => $quiz->id]);
|
||||
|
||||
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.
|
||||
quiz_set_grade(0, $quiz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sumgrades field of the attempts at a quiz.
|
||||
*
|
||||
* @param stdClass $quiz a quiz.
|
||||
*/
|
||||
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')}
|
||||
)
|
||||
WHERE quiz = :quizid AND state = :finishedstate";
|
||||
$DB->execute($sql, ['timenow' => $timenow, 'quizid' => $quiz->id,
|
||||
'finishedstate' => quiz_attempt::FINISHED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The quiz grade is the maximum that student's results are marked out of. When it
|
||||
* changes, the corresponding data in quiz_grades and quiz_feedback needs to be
|
||||
* rescaled. After calling this function, you probably need to call
|
||||
* quiz_update_all_attempt_sumgrades, quiz_update_all_final_grades and
|
||||
* quiz_update_grades.
|
||||
*
|
||||
* @param float $newgrade the new maximum grade for the quiz.
|
||||
* @param stdClass $quiz the quiz we are updating. Passed by reference so its
|
||||
* grade field can be updated too.
|
||||
* @return bool indicating success or failure.
|
||||
*/
|
||||
function quiz_set_grade($newgrade, $quiz) {
|
||||
global $DB;
|
||||
// This is potentially expensive, so only do it if necessary.
|
||||
if (abs($quiz->grade - $newgrade) < 1e-7) {
|
||||
// Nothing to do.
|
||||
return true;
|
||||
}
|
||||
|
||||
$oldgrade = $quiz->grade;
|
||||
$quiz->grade = $newgrade;
|
||||
|
||||
// Use a transaction, so that on those databases that support it, this is safer.
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
|
||||
// Update the quiz table.
|
||||
$DB->set_field('quiz', 'grade', $newgrade, ['id' => $quiz->instance]);
|
||||
|
||||
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);
|
||||
|
||||
} else {
|
||||
// We can rescale the grades efficiently.
|
||||
$timemodified = time();
|
||||
$DB->execute("
|
||||
UPDATE {quiz_grades}
|
||||
SET grade = ? * grade, timemodified = ?
|
||||
WHERE quiz = ?
|
||||
", [$newgrade / $oldgrade, $timemodified, $quiz->id]);
|
||||
}
|
||||
|
||||
if ($oldgrade > 1e-7) {
|
||||
// Update the quiz_feedback table.
|
||||
$factor = $newgrade/$oldgrade;
|
||||
$DB->execute("
|
||||
UPDATE {quiz_feedback}
|
||||
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
|
||||
WHERE quizid = ?
|
||||
", [$factor, $factor, $quiz->id]);
|
||||
}
|
||||
|
||||
// Update grade item and send all grades to gradebook.
|
||||
quiz_grade_item_update($quiz);
|
||||
quiz_update_grades($quiz);
|
||||
|
||||
$transaction->allow_commit();
|
||||
|
||||
// Log quiz grade updated event.
|
||||
// We use $num + 0 as a trick to remove the useless 0 digits from decimals.
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
|
||||
$event = \mod_quiz\event\quiz_grade_updated::create([
|
||||
'context' => \context_module::instance($cm->id),
|
||||
'objectid' => $quiz->id,
|
||||
'other' => [
|
||||
'oldgrade' => $oldgrade + 0,
|
||||
'newgrade' => $newgrade + 0
|
||||
]
|
||||
]);
|
||||
$event->trigger();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the overall grade for a user at a quiz in the quiz_grades table
|
||||
*
|
||||
* @param stdClass $quiz The quiz for which the best grade is to be calculated and then saved.
|
||||
* @param int $userid The userid to calculate the grade for. Defaults to the current user.
|
||||
* @param array $attempts The attempts of this user. Useful if you are
|
||||
* looping through many users. Attempts can be fetched in one master query to
|
||||
* avoid repeated querying.
|
||||
* @return bool Indicates success or failure.
|
||||
*/
|
||||
function quiz_save_best_grade($quiz, $userid = null, $attempts = []) {
|
||||
global $DB, $OUTPUT, $USER;
|
||||
|
||||
if (empty($userid)) {
|
||||
$userid = $USER->id;
|
||||
}
|
||||
|
||||
if (!$attempts) {
|
||||
// Get all the attempts made by the user.
|
||||
$attempts = quiz_get_user_attempts($quiz->id, $userid);
|
||||
}
|
||||
|
||||
// Calculate the best grade.
|
||||
$bestgrade = quiz_calculate_best_grade($quiz, $attempts);
|
||||
$bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
|
||||
|
||||
// Save the best grade in the database.
|
||||
if (is_null($bestgrade)) {
|
||||
$DB->delete_records('quiz_grades', ['quiz' => $quiz->id, 'userid' => $userid]);
|
||||
|
||||
} else if ($grade = $DB->get_record('quiz_grades',
|
||||
['quiz' => $quiz->id, 'userid' => $userid])) {
|
||||
$grade->grade = $bestgrade;
|
||||
$grade->timemodified = time();
|
||||
$DB->update_record('quiz_grades', $grade);
|
||||
|
||||
} else {
|
||||
$grade = new stdClass();
|
||||
$grade->quiz = $quiz->id;
|
||||
$grade->userid = $userid;
|
||||
$grade->grade = $bestgrade;
|
||||
$grade->timemodified = time();
|
||||
$DB->insert_record('quiz_grades', $grade);
|
||||
}
|
||||
|
||||
quiz_update_grades($quiz, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the overall grade for a quiz given a number of attempts by a particular user.
|
||||
*
|
||||
* @param stdClass $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
|
||||
*/
|
||||
function quiz_calculate_best_grade($quiz, $attempts) {
|
||||
|
||||
switch ($quiz->grademethod) {
|
||||
|
||||
case QUIZ_ATTEMPTFIRST:
|
||||
$firstattempt = reset($attempts);
|
||||
return $firstattempt->sumgrades;
|
||||
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
$lastattempt = end($attempts);
|
||||
return $lastattempt->sumgrades;
|
||||
|
||||
case QUIZ_GRADEAVERAGE:
|
||||
$sum = 0;
|
||||
$count = 0;
|
||||
foreach ($attempts as $attempt) {
|
||||
if (!is_null($attempt->sumgrades)) {
|
||||
$sum += $attempt->sumgrades;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
if ($count == 0) {
|
||||
return null;
|
||||
}
|
||||
return $sum / $count;
|
||||
|
||||
case QUIZ_GRADEHIGHEST:
|
||||
default:
|
||||
$max = null;
|
||||
foreach ($attempts as $attempt) {
|
||||
if ($attempt->sumgrades > $max) {
|
||||
$max = $attempt->sumgrades;
|
||||
}
|
||||
}
|
||||
return $max;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 stdClass $quiz the quiz settings.
|
||||
*/
|
||||
function quiz_update_all_final_grades($quiz) {
|
||||
global $DB;
|
||||
|
||||
if (!$quiz->sumgrades) {
|
||||
return;
|
||||
}
|
||||
|
||||
$param = ['iquizid' => $quiz->id, 'istatefinished' => quiz_attempt::FINISHED];
|
||||
$firstlastattemptjoin = "JOIN (
|
||||
SELECT
|
||||
iquiza.userid,
|
||||
MIN(attempt) AS firstattempt,
|
||||
MAX(attempt) AS lastattempt
|
||||
|
||||
FROM {quiz_attempts} iquiza
|
||||
|
||||
WHERE
|
||||
iquiza.state = :istatefinished AND
|
||||
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:
|
||||
// Because of the where clause, there will only be one row, but we
|
||||
// must still use an aggregate function.
|
||||
$select = 'MAX(quiza.sumgrades)';
|
||||
$join = $firstlastattemptjoin;
|
||||
$where = 'quiza.attempt = first_last_attempts.firstattempt AND';
|
||||
break;
|
||||
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
// Because of the where clause, there will only be one row, but we
|
||||
// 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;
|
||||
$param['statefinished'] = quiz_attempt::FINISHED;
|
||||
$param['statefinished2'] = quiz_attempt::FINISHED;
|
||||
$finalgradesubquery = "
|
||||
SELECT quiza.userid, $finalgrade AS newgrade
|
||||
FROM {quiz_attempts} quiza
|
||||
$join
|
||||
WHERE
|
||||
$where
|
||||
quiza.state = :statefinished AND
|
||||
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
|
||||
quiza2.state = :statefinished2 AND
|
||||
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
|
||||
((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).
|
||||
$param);
|
||||
|
||||
$timenow = time();
|
||||
$todelete = [];
|
||||
foreach ($changedgrades as $changedgrade) {
|
||||
|
||||
if (is_null($changedgrade->newgrade)) {
|
||||
$todelete[] = $changedgrade->userid;
|
||||
|
||||
} else if (is_null($changedgrade->grade)) {
|
||||
$toinsert = new stdClass();
|
||||
$toinsert->quiz = $quiz->id;
|
||||
$toinsert->userid = $changedgrade->userid;
|
||||
$toinsert->timemodified = $timenow;
|
||||
$toinsert->grade = $changedgrade->newgrade;
|
||||
$DB->insert_record('quiz_grades', $toinsert);
|
||||
|
||||
} else {
|
||||
$toupdate = new stdClass();
|
||||
$toupdate->id = $changedgrade->id;
|
||||
$toupdate->grade = $changedgrade->newgrade;
|
||||
$toupdate->timemodified = $timenow;
|
||||
$DB->update_record('quiz_grades', $toupdate);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($todelete)) {
|
||||
list($test, $params) = $DB->get_in_or_equal($todelete);
|
||||
$DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test,
|
||||
array_merge([$quiz->id], $params));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return summary of the number of settings override that exist.
|
||||
*
|
||||
@@ -1000,14 +637,14 @@ function quiz_update_all_final_grades($quiz) {
|
||||
* quiz renderer method.
|
||||
*
|
||||
* @param stdClass $quiz the quiz settings. Only $quiz->id is used at the moment.
|
||||
* @param stdClass|cm_info $cm the cm object. Only $cm->course, $cm->groupmode and
|
||||
* @param cm_info|stdClass $cm the cm object. Only $cm->course, $cm->groupmode and
|
||||
* $cm->groupingid fields are used at the moment.
|
||||
* @param int $currentgroup if there is a concept of current group where this method is being called
|
||||
* (e.g. a report) pass it in here. Default 0 which means no current group.
|
||||
* @return array like 'group' => 3, 'user' => 12] where 3 is the number of group overrides,
|
||||
* and 12 is the number of user ones.
|
||||
*/
|
||||
function quiz_override_summary(stdClass $quiz, stdClass $cm, int $currentgroup = 0): array {
|
||||
function quiz_override_summary(stdClass $quiz, cm_info|stdClass $cm, int $currentgroup = 0): array {
|
||||
global $DB;
|
||||
|
||||
if ($currentgroup) {
|
||||
@@ -1213,45 +850,6 @@ function quiz_get_attempt_usertime_sql($redundantwhereclauses = '') {
|
||||
return $quizausersql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 stdClass The attempt with the best grade
|
||||
* @param stdClass $quiz The quiz for which the best grade is to be calculated
|
||||
* @param array $attempts An array of all the attempts of the user at the quiz
|
||||
*/
|
||||
function quiz_calculate_best_attempt($quiz, $attempts) {
|
||||
|
||||
switch ($quiz->grademethod) {
|
||||
|
||||
case QUIZ_ATTEMPTFIRST:
|
||||
foreach ($attempts as $attempt) {
|
||||
return $attempt;
|
||||
}
|
||||
break;
|
||||
|
||||
case QUIZ_GRADEAVERAGE: // We need to do something with it.
|
||||
case QUIZ_ATTEMPTLAST:
|
||||
foreach ($attempts as $attempt) {
|
||||
$final = $attempt;
|
||||
}
|
||||
return $final;
|
||||
|
||||
default:
|
||||
case QUIZ_GRADEHIGHEST:
|
||||
$max = -1;
|
||||
foreach ($attempts as $attempt) {
|
||||
if ($attempt->sumgrades > $max) {
|
||||
$max = $attempt->sumgrades;
|
||||
$maxattempt = $attempt;
|
||||
}
|
||||
}
|
||||
return $maxattempt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array int => lang string the options for calculating the quiz grade
|
||||
* from the individual attempt grades.
|
||||
@@ -1886,15 +1484,13 @@ function quiz_send_overdue_message($attemptobj) {
|
||||
*
|
||||
* This sends the confirmation and notification messages, if required.
|
||||
*
|
||||
* @param stdClass $event the event object.
|
||||
* @param attempt_submitted $event the event object.
|
||||
*/
|
||||
function quiz_attempt_submitted_handler($event) {
|
||||
global $DB;
|
||||
|
||||
$course = $DB->get_record('course', ['id' => $event->courseid]);
|
||||
$course = get_course($event->courseid);
|
||||
$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);
|
||||
$quiz = $event->get_record_snapshot('quiz', $attempt->quiz);
|
||||
$cm = get_coursemodule_from_id('quiz', $event->get_context()->instanceid, $event->courseid);
|
||||
$eventdata = $event->get_data();
|
||||
|
||||
if (!($course && $quiz && $cm && $attempt)) {
|
||||
@@ -2204,7 +1800,7 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null)
|
||||
|
||||
if (!$qreferenceitem) {
|
||||
// Create a new reference record for questions created already.
|
||||
$questionreferences = new \StdClass();
|
||||
$questionreferences = new stdClass();
|
||||
$questionreferences->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$questionreferences->component = 'mod_quiz';
|
||||
$questionreferences->questionarea = 'slot';
|
||||
@@ -2214,13 +1810,13 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null)
|
||||
$DB->insert_record('question_references', $questionreferences);
|
||||
|
||||
} else if ($qreferenceitem->itemid === 0 || $qreferenceitem->itemid === null) {
|
||||
$questionreferences = new \StdClass();
|
||||
$questionreferences = new stdClass();
|
||||
$questionreferences->id = $qreferenceitem->id;
|
||||
$questionreferences->itemid = $slotid;
|
||||
$DB->update_record('question_references', $questionreferences);
|
||||
} else {
|
||||
// If the reference record exits for another quiz.
|
||||
$questionreferences = new \StdClass();
|
||||
$questionreferences = new stdClass();
|
||||
$questionreferences->usingcontextid = context_module::instance($quiz->cmid)->id;
|
||||
$questionreferences->component = 'mod_quiz';
|
||||
$questionreferences->questionarea = 'slot';
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
use mod_quiz\form\edit_override_form;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot.'/mod/quiz/lib.php');
|
||||
@@ -31,18 +32,12 @@ require_once($CFG->dirroot.'/mod/quiz/locallib.php');
|
||||
$overrideid = required_param('id', PARAM_INT);
|
||||
$confirm = optional_param('confirm', false, PARAM_BOOL);
|
||||
|
||||
if (! $override = $DB->get_record('quiz_overrides', ['id' => $overrideid])) {
|
||||
throw new \moodle_exception('invalidoverrideid', 'quiz');
|
||||
}
|
||||
if (! $quiz = $DB->get_record('quiz', ['id' => $override->quiz])) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $quiz->course)) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
|
||||
|
||||
$context = context_module::instance($cm->id);
|
||||
$override = $DB->get_record('quiz_overrides', ['id' => $overrideid], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create($override->quiz);
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
$context = $quizobj->get_context();
|
||||
|
||||
require_login($course, false, $cm);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
use mod_quiz\form\edit_override_form;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot.'/mod/quiz/lib.php');
|
||||
@@ -35,23 +36,16 @@ $reset = optional_param('reset', false, PARAM_BOOL);
|
||||
|
||||
$override = null;
|
||||
if ($overrideid) {
|
||||
|
||||
if (! $override = $DB->get_record('quiz_overrides', ['id' => $overrideid])) {
|
||||
throw new \moodle_exception('invalidoverrideid', 'quiz');
|
||||
}
|
||||
if (! $quiz = $DB->get_record('quiz', ['id' => $override->quiz])) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
list($course, $cm) = get_course_and_cm_from_instance($quiz, 'quiz');
|
||||
|
||||
} else if ($cmid) {
|
||||
list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'quiz');
|
||||
$quiz = $DB->get_record('quiz', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
|
||||
$override = $DB->get_record('quiz_overrides', ['id' => $overrideid], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create($override->quiz);
|
||||
} else {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
$quizobj = quiz_settings::create_for_cmid($cmid);
|
||||
}
|
||||
$course = $DB->get_record('course', ['id' => $cm->course], '*', MUST_EXIST);
|
||||
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
$context = $quizobj->get_context();
|
||||
|
||||
$url = new moodle_url('/mod/quiz/overrideedit.php');
|
||||
if ($action) {
|
||||
@@ -70,8 +64,6 @@ $PAGE->set_secondary_active_tab("mod_quiz_useroverrides");
|
||||
|
||||
require_login($course, false, $cm);
|
||||
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
// Add or edit an override.
|
||||
require_capability('mod/quiz:manageoverrides', $context);
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot.'/mod/quiz/lib.php');
|
||||
require_once($CFG->dirroot.'/mod/quiz/locallib.php');
|
||||
@@ -29,13 +31,14 @@ require_once($CFG->dirroot.'/mod/quiz/locallib.php');
|
||||
$cmid = required_param('cmid', PARAM_INT);
|
||||
$mode = optional_param('mode', '', PARAM_ALPHA); // One of 'user' or 'group', default is 'group'.
|
||||
|
||||
list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'quiz');
|
||||
$quiz = $DB->get_record('quiz', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
$quizobj = quiz_settings::create_for_cmid($cmid);
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
$context = $quizobj->get_context();
|
||||
|
||||
require_login($course, false, $cm);
|
||||
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
// Check the user has the required capabilities to list overrides.
|
||||
$canedit = has_capability('mod/quiz:manageoverrides', $context);
|
||||
if (!$canedit) {
|
||||
|
||||
+9
-22
@@ -22,6 +22,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
define('NO_OUTPUT_BUFFERING', true);
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
@@ -33,27 +35,13 @@ $q = optional_param('q', 0, PARAM_INT);
|
||||
$mode = optional_param('mode', '', PARAM_ALPHA);
|
||||
|
||||
if ($id) {
|
||||
if (!$cm = get_coursemodule_from_id('quiz', $id)) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
if (!$course = $DB->get_record('course', ['id' => $cm->course])) {
|
||||
throw new \moodle_exception('coursemisconf');
|
||||
}
|
||||
if (!$quiz = $DB->get_record('quiz', ['id' => $cm->instance])) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
|
||||
$quizobj = quiz_settings::create_for_cmid($id);
|
||||
} else {
|
||||
if (!$quiz = $DB->get_record('quiz', ['id' => $q])) {
|
||||
throw new \moodle_exception('invalidquizid', 'quiz');
|
||||
}
|
||||
if (!$course = $DB->get_record('course', ['id' => $quiz->course])) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
$quizobj = quiz_settings::create($q);
|
||||
}
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
|
||||
$url = new moodle_url('/mod/quiz/report.php', ['id' => $cm->id]);
|
||||
if ($mode !== '') {
|
||||
@@ -62,10 +50,9 @@ if ($mode !== '') {
|
||||
$PAGE->set_url($url);
|
||||
|
||||
require_login($course, false, $cm);
|
||||
$context = context_module::instance($cm->id);
|
||||
$PAGE->set_pagelayout('report');
|
||||
$PAGE->activityheader->disable();
|
||||
$reportlist = quiz_report_list($context);
|
||||
$reportlist = quiz_report_list($quizobj->get_context());
|
||||
if (empty($reportlist)) {
|
||||
throw new \moodle_exception('erroraccessingreport', 'quiz');
|
||||
}
|
||||
@@ -100,7 +87,7 @@ echo $OUTPUT->footer();
|
||||
|
||||
// Log that this report was viewed.
|
||||
$params = [
|
||||
'context' => $context,
|
||||
'context' => $quizobj->get_context(),
|
||||
'other' => [
|
||||
'quizid' => $quiz->id,
|
||||
'reportname' => $mode
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
use mod_quiz\local\reports\attempts_report;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -54,7 +55,7 @@ class quiz_overview_report extends attempts_report {
|
||||
protected $newquestionidsforold = null;
|
||||
|
||||
public function display($quiz, $cm, $course) {
|
||||
global $DB, $OUTPUT, $PAGE;
|
||||
global $DB, $PAGE;
|
||||
|
||||
list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->init(
|
||||
'overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
|
||||
@@ -686,8 +687,9 @@ class quiz_overview_report extends attempts_report {
|
||||
* @param stdClass $quiz the quiz settings.
|
||||
*/
|
||||
protected function update_overall_grades($quiz) {
|
||||
quiz_update_all_attempt_sumgrades($quiz);
|
||||
quiz_update_all_final_grades($quiz);
|
||||
$gradecalculator = $this->quizobj->get_grade_calculator();
|
||||
$gradecalculator->recompute_all_attempt_sumgrades();
|
||||
$gradecalculator->recompute_all_final_grades();
|
||||
quiz_update_grades($quiz);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class report_test extends \advanced_testcase {
|
||||
$update->timemodified = $timestart + 1200;
|
||||
$update->sumgrades = $quba->get_total_mark();
|
||||
$DB->update_record('quiz_attempts', $update);
|
||||
quiz_save_best_grade($attemptobj->get_quiz(), $student->id);
|
||||
$attemptobj->get_quizobj()->get_grade_calculator()->recompute_final_grade($student->id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ class quiz_statistics_report extends report_base {
|
||||
$this->output_quiz_structure_analysis_table($questionstats);
|
||||
|
||||
if ($this->table->is_downloading() == 'html' && $quizstats->s() != 0) {
|
||||
$this->output_statistics_graph($quiz->id, $qubaids);
|
||||
$this->output_statistics_graph($quiz, $qubaids);
|
||||
}
|
||||
|
||||
$this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
|
||||
@@ -537,18 +537,12 @@ class quiz_statistics_report extends report_base {
|
||||
/**
|
||||
* Output the HTML needed to show the statistics graph.
|
||||
*
|
||||
* @param int|object $quizorid The quiz, or its ID.
|
||||
* @param stdClass $quiz the quiz.
|
||||
* @param qubaid_condition $qubaids the question usages whose responses to analyse.
|
||||
* @param string $whichattempts Which attempts constant.
|
||||
*/
|
||||
protected function output_statistics_graph($quizorid, $qubaids) {
|
||||
protected function output_statistics_graph($quiz, $qubaids) {
|
||||
global $DB, $PAGE;
|
||||
|
||||
$quiz = $quizorid;
|
||||
if (!is_object($quiz)) {
|
||||
$quiz = $DB->get_record('quiz', ['id' => $quizorid], '*', MUST_EXIST);
|
||||
}
|
||||
|
||||
// Load the rest of the required data.
|
||||
$questions = quiz_report_get_significant_questions($quiz);
|
||||
|
||||
|
||||
@@ -37,14 +37,8 @@ $id = required_param('cmid', PARAM_INT); // Course module id
|
||||
$forcenew = optional_param('forcenew', false, PARAM_BOOL); // Used to force a new preview
|
||||
$page = optional_param('page', -1, PARAM_INT); // Page to jump to in the attempt.
|
||||
|
||||
if (!$cm = get_coursemodule_from_id('quiz', $id)) {
|
||||
throw new \moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
if (!$course = $DB->get_record('course', ['id' => $cm->course])) {
|
||||
throw new \moodle_exception("coursemisconf");
|
||||
}
|
||||
$quizobj = quiz_settings::create_for_cmid($id, $USER->id);
|
||||
|
||||
$quizobj = quiz_settings::create($cm->instance, $USER->id);
|
||||
// This script should only ever be posted to, so set page URL to the view page.
|
||||
$PAGE->set_url($quizobj->view_url());
|
||||
// During quiz attempts, the browser back/forwards buttons should force a reload.
|
||||
|
||||
@@ -175,8 +175,9 @@ class attempt_walkthrough_test extends \advanced_testcase {
|
||||
$saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
|
||||
// Add them to the quiz.
|
||||
$quizobj = quiz_settings::create($quiz->id);
|
||||
quiz_add_quiz_question($saq->id, $quiz, 0, 1);
|
||||
quiz_update_sumgrades($quiz);
|
||||
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
|
||||
|
||||
return $quiz;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use Behat\Gherkin\Node\TableNode as TableNode;
|
||||
|
||||
use Behat\Mink\Exception\ExpectationException as ExpectationException;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
/**
|
||||
* Steps definitions related to mod_quiz.
|
||||
@@ -307,7 +308,8 @@ class behat_mod_quiz extends behat_question_base {
|
||||
}
|
||||
}
|
||||
|
||||
quiz_update_sumgrades($quiz);
|
||||
$quizobj = quiz_settings::create($quiz->id);
|
||||
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,7 +113,7 @@ class custom_completion_test extends advanced_testcase {
|
||||
* @param array $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int
|
||||
*/
|
||||
private function do_attempt_quiz(array $attemptoptions) {
|
||||
$quizobj = quiz_settings::create($attemptoptions['quiz']->id);
|
||||
$quizobj = quiz_settings::create((int) $attemptoptions['quiz']->id);
|
||||
|
||||
// Start the passing attempt.
|
||||
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||
|
||||
@@ -150,7 +150,7 @@ class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcas
|
||||
$update->timemodified = $timenow;
|
||||
$update->sumgrades = $attemptobj1->get_question_usage()->get_total_mark();
|
||||
$DB->update_record('quiz_attempts', $update);
|
||||
quiz_save_best_grade($attemptobj1->get_quiz(), $this->student->id);
|
||||
$attemptobj1->get_quizobj()->get_grade_calculator()->recompute_final_grade();
|
||||
|
||||
// Not quite time to send yet.
|
||||
$task = new quiz_notify_attempt_manual_grading_completed();
|
||||
@@ -217,7 +217,7 @@ class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcas
|
||||
$update->timemodified = $timenow;
|
||||
$update->sumgrades = $attemptobj->get_question_usage()->get_total_mark();
|
||||
$DB->update_record('quiz_attempts', $update);
|
||||
quiz_save_best_grade($attemptobj->get_quiz(), $this->student->id);
|
||||
$attemptobj->get_quizobj()->get_grade_calculator()->recompute_final_grade();
|
||||
|
||||
// Run the quiz notify attempt manual graded task.
|
||||
ob_start();
|
||||
@@ -257,7 +257,7 @@ class quiz_notify_attempt_manual_grading_completed_test extends advanced_testcas
|
||||
$update->timemodified = $timenow;
|
||||
$update->sumgrades = $attemptobj->get_question_usage()->get_total_mark();
|
||||
$DB->update_record('quiz_attempts', $update);
|
||||
quiz_save_best_grade($attemptobj->get_quiz(), $this->student->id);
|
||||
$attemptobj->get_quizobj()->get_grade_calculator()->recompute_final_grade();
|
||||
|
||||
// Run the quiz notify attempt manual graded task.
|
||||
ob_start();
|
||||
|
||||
+14
-2
@@ -4,9 +4,9 @@ This files describes API changes in the quiz code.
|
||||
|
||||
* The methods in the quiz_settings class which return a URL now all return a moodle_url. Previously
|
||||
some returns a moodle_url and others aa string.
|
||||
* The method quiz_settings::confirm_start_attempt_message, which was deprecated in Moodle 3.1, is now completely removed.
|
||||
* The field view_page::$startattemptwarning, which was deprecated in Moodle 3.1, is now completely removed.
|
||||
|
||||
* 'firstslotid' value is not used in section_shuffle_updated event anymore.
|
||||
|
||||
* The quiz has a lot of old classes in lib.php files. These have now been moved into the classes folder,
|
||||
and so are now in namespaces. Because of Moodle's class renaming support, your code should continue
|
||||
working, but output deprecated warnings, so you probably want to update. This should mostly be
|
||||
@@ -77,11 +77,23 @@ This files describes API changes in the quiz code.
|
||||
- mod/quiz/renderer.php - actually, no debugging ouput for this one because of how renderer factories work.
|
||||
- mod/quiz/attemptlib.php
|
||||
|
||||
* Various functions related to calculating grades have moved into a new class mod_quiz\grade_calculator.
|
||||
You get that using $quizobj->get_grade_calculator(), then the following old functions have become these new methods.
|
||||
- quiz_update_sumgrades -> recompute_quiz_sumgrades
|
||||
- quiz_update_all_attempt_sumgrades -> recompute_all_attempt_sumgrades
|
||||
- quiz_update_all_final_grades -> recompute_all_final_grades
|
||||
- quiz_set_grade -> update_quiz_maximum_grade
|
||||
- quiz_save_best_grade -> recompute_final_grade
|
||||
- quiz_calculate_best_grade -> [no public direct replacement]
|
||||
- quiz_calculate_best_attempt - [no replacement. It was not used.]
|
||||
|
||||
* Final deprecation (complete removal) of the following functions which were deprecated long ago:
|
||||
- quiz_groups_member_added_handler - deprecated since 2.6
|
||||
- quiz_groups_member_removed_handler - deprecated since 2.6
|
||||
- quiz_groups_group_deleted_handler - deprecated since 2.6
|
||||
- quiz_groups_members_removed_handler - deprecated since 2.6
|
||||
- The method quiz_settings::confirm_start_attempt_message - deprecated in Moodle 3.1
|
||||
- The field view_page::$startattemptwarning - deprecated in Moodle 3.1
|
||||
- attempts_report::load_relevant_students - deprecated since 3.2
|
||||
- quiz_statistics_graph_get_new_colour - deprecated since 3.2
|
||||
- The file mod/quiz/report/overview/overviewgraph.php - deprecated since 3.2
|
||||
|
||||
+6
-18
@@ -39,27 +39,17 @@ $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or ...
|
||||
$q = optional_param('q', 0, PARAM_INT); // Quiz ID.
|
||||
|
||||
if ($id) {
|
||||
if (!$cm = get_coursemodule_from_id('quiz', $id)) {
|
||||
throw new moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
if (!$course = $DB->get_record('course', ['id' => $cm->course])) {
|
||||
throw new moodle_exception('coursemisconf');
|
||||
}
|
||||
$quizobj = quiz_settings::create_for_cmid($id, $USER->id);
|
||||
} else {
|
||||
if (!$quiz = $DB->get_record('quiz', ['id' => $q])) {
|
||||
throw new moodle_exception('invalidquizid', 'quiz');
|
||||
}
|
||||
if (!$course = $DB->get_record('course', ['id' => $quiz->course])) {
|
||||
throw new moodle_exception('invalidcourseid');
|
||||
}
|
||||
if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
|
||||
throw new moodle_exception('invalidcoursemodule');
|
||||
}
|
||||
$quizobj = quiz_settings::create($q, $USER->id);
|
||||
}
|
||||
$quiz = $quizobj->get_quiz();
|
||||
$cm = $quizobj->get_cm();
|
||||
$course = $quizobj->get_course();
|
||||
|
||||
// Check login and get context.
|
||||
require_login($course, false, $cm);
|
||||
$context = context_module::instance($cm->id);
|
||||
$context = $quizobj->get_context();
|
||||
require_capability('mod/quiz:view', $context);
|
||||
|
||||
// Cache some other capabilities we use several times.
|
||||
@@ -69,10 +59,8 @@ $canpreview = has_capability('mod/quiz:preview', $context);
|
||||
|
||||
// Create an object to manage all the other (non-roles) access rules.
|
||||
$timenow = time();
|
||||
$quizobj = quiz_settings::create($cm->instance, $USER->id);
|
||||
$accessmanager = new access_manager($quizobj, $timenow,
|
||||
has_capability('mod/quiz:ignoretimelimits', $context, null, false));
|
||||
$quiz = $quizobj->get_quiz();
|
||||
|
||||
// Trigger course_module_viewed event and completion.
|
||||
quiz_view($quiz, $course, $cm, $context);
|
||||
|
||||
@@ -1173,11 +1173,11 @@ ORDER BY
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a subquery that computes the sum of the marks for all the questions
|
||||
* in a usage. Which useage to compute the sum for is controlled bu the $qubaid
|
||||
* Return a sub-query that computes the sum of the marks for all the questions
|
||||
* in a usage. Which usage to compute the sum for is controlled by the $qubaid
|
||||
* parameter.
|
||||
*
|
||||
* See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
|
||||
* See {@see \mod_quiz\grade_calculator::recompute_all_attempt_sumgrades()} for an example of the usage of
|
||||
* this method.
|
||||
*
|
||||
* This method may be called publicly.
|
||||
|
||||
Reference in New Issue
Block a user