This commit is contained in:
Jake Dallimore
2023-03-01 12:43:58 +08:00
40 changed files with 874 additions and 624 deletions
@@ -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());
+2 -2
View File
@@ -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);
+5 -5
View File
@@ -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
View File
@@ -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);
+2 -2
View File
@@ -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;
+439
View File
@@ -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,
+1 -1
View File
@@ -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);
+3 -3
View File
@@ -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);
+26 -10
View File
@@ -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()
]
+46 -11
View File
@@ -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.
*
+10
View File
@@ -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.
+1 -1
View File
@@ -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(),
+179
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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';
+7 -12
View File
@@ -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);
+9 -17
View File
@@ -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);
+7 -4
View File
@@ -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
View File
@@ -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
+5 -3
View File
@@ -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;
}
}
+3 -9
View File
@@ -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);
+1 -7
View File
@@ -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.
+2 -1
View File
@@ -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;
}
+3 -1
View File
@@ -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();
}
/**
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+3 -3
View File
@@ -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.