MDL-80099 quiz: Add attempt_state_changed hook

This creates a new hook which is dispatched when a quiz attempt is set
to a new state, or deleted. This is then used by quiz_statistics to
trigger a recalulation, replacing the old event observer (for
submissions) and class callback (for deletions).
This commit is contained in:
Mark Johnson 2023-12-05 15:54:28 +00:00
parent 9a2f82a709
commit ee952d6556
No known key found for this signature in database
GPG Key ID: EB30E1468CFAE242
11 changed files with 130 additions and 40 deletions

View File

@ -0,0 +1,71 @@
<?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\hook;
use core\attribute;
/**
* A quiz attempt changed state.
*
* @package mod_quiz
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
#[attribute\label('A quiz attempt changed state.')]
#[attribute\tags('quiz', 'attempt')]
#[attribute\hook\replaces_callbacks('quiz_attempt_deleted::callback')]
class attempt_state_changed {
/**
* Create a new hook instance.
*
* @param ?\stdClass $originalattempt The original database record for the attempt, null if it has just been created.
* @param ?\stdClass $updatedattempt The updated database record of the new attempt, null if it has just been deleted.
*/
public function __construct(
protected ?\stdClass $originalattempt,
protected ?\stdClass $updatedattempt,
) {
if (is_null($this->originalattempt) && is_null($this->updatedattempt)) {
throw new \InvalidArgumentException('originalattempt and updatedattempt cannot both be null.');
}
if (
!is_null($this->originalattempt)
&& !is_null($this->updatedattempt)
&& $this->originalattempt->id != $this->updatedattempt->id
) {
throw new \InvalidArgumentException('originalattempt and updatedattempt must have the same id.');
}
}
/**
* Get the original attempt, null if it has just been created.
*
* @return ?\stdClass
*/
public function get_original_attempt(): ?\stdClass {
return $this->originalattempt;
}
/**
* Get the updated attempt, null if it has just been deleted.
*
* @return ?\stdClass
*/
public function get_updated_attempt(): ?\stdClass {
return $this->updatedattempt;
}
}

View File

@ -23,6 +23,7 @@ use coding_exception;
use context_module;
use Exception;
use html_writer;
use mod_quiz\hook\attempt_state_changed;
use mod_quiz\output\links_to_other_attempts;
use mod_quiz\output\renderer;
use mod_quiz\question\bank\qbank_helper;
@ -1763,6 +1764,8 @@ class quiz_attempt {
question_engine::save_questions_usage_by_activity($this->quba);
$originalattempt = clone $this->attempt;
$this->attempt->timemodified = $timestamp;
$this->attempt->timefinish = $timefinish ?? $timestamp;
$this->attempt->sumgrades = $this->quba->get_total_mark();
@ -1784,6 +1787,7 @@ class quiz_attempt {
// Trigger event.
$this->fire_state_transition_event('\mod_quiz\event\attempt_submitted', $timestamp, $studentisonline);
\core\hook\manager::get_instance()->dispatch(new attempt_state_changed($originalattempt, $this->attempt));
// Tell any access rules that care that the attempt is over.
$this->get_access_manager($timestamp)->current_attempt_finished();
}
@ -1820,6 +1824,7 @@ class quiz_attempt {
public function process_going_overdue($timestamp, $studentisonline) {
global $DB;
$originalattempt = clone $this->attempt;
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::OVERDUE;
@ -1830,6 +1835,7 @@ class quiz_attempt {
$this->fire_state_transition_event('\mod_quiz\event\attempt_becameoverdue', $timestamp, $studentisonline);
\core\hook\manager::get_instance()->dispatch(new attempt_state_changed($originalattempt, $this->attempt));
$transaction->allow_commit();
quiz_send_overdue_message($this);
@ -1844,6 +1850,7 @@ class quiz_attempt {
public function process_abandon($timestamp, $studentisonline) {
global $DB;
$originalattempt = clone $this->attempt;
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::ABANDONED;
@ -1852,6 +1859,8 @@ class quiz_attempt {
$this->fire_state_transition_event('\mod_quiz\event\attempt_abandoned', $timestamp, $studentisonline);
\core\hook\manager::get_instance()->dispatch(new attempt_state_changed($originalattempt, $this->attempt));
$transaction->allow_commit();
}
@ -1872,6 +1881,7 @@ class quiz_attempt {
throw new coding_exception('Can only reopen an attempt that was never submitted.');
}
$originalattempt = clone $this->attempt;
$transaction = $DB->start_delegated_transaction();
$this->attempt->timemodified = $timestamp;
$this->attempt->state = self::IN_PROGRESS;
@ -1880,6 +1890,7 @@ class quiz_attempt {
$this->fire_state_transition_event('\mod_quiz\event\attempt_reopened', $timestamp, false);
\core\hook\manager::get_instance()->dispatch(new attempt_state_changed($originalattempt, $this->attempt));
$timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt);
if ($timeclose && $timestamp > $timeclose) {
$this->process_finish($timestamp, false, $timeclose);

View File

@ -39,6 +39,7 @@ use core_question\local\bank\condition;
use mod_quiz\access_manager;
use mod_quiz\event\attempt_submitted;
use mod_quiz\grade_calculator;
use mod_quiz\hook\attempt_state_changed;
use mod_quiz\question\bank\qbank_helper;
use mod_quiz\question\display_options;
use mod_quiz\quiz_attempt;
@ -145,6 +146,8 @@ function quiz_create_attempt(quiz_settings $quizobj, $attemptnumber, $lastattemp
$attempt->timecheckstate = $timeclose;
}
\core\hook\manager::get_instance()->dispatch(new attempt_state_changed(null, $attempt));
return $attempt;
}
/**
@ -448,10 +451,14 @@ function quiz_delete_attempt($attempt, $quiz) {
$event->add_record_snapshot('quiz_attempts', $attempt);
$event->trigger();
// This class callback is deprecated, and will be removed in Moodle 4.8 (MDL-80327).
// Use the attempt_state_changed hook instead.
$callbackclasses = \core_component::get_plugin_list_with_class('quiz', 'quiz_attempt_deleted');
foreach ($callbackclasses as $callbackclass) {
component_class_callback($callbackclass, 'callback', [$quiz->id]);
component_class_callback($callbackclass, 'callback', [$quiz->id], null, true);
}
\core\hook\manager::get_instance()->dispatch(new attempt_state_changed($attempt, null));
}
// Search quiz_attempts for other instances by this user.

View File

@ -16,6 +16,7 @@
namespace quiz_statistics\event\observer;
use core\check\performance\debugging;
use quiz_statistics\task\recalculate;
/**
@ -25,6 +26,8 @@ use quiz_statistics\task\recalculate;
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Since Moodle 4.4 MDL-80099.
* @todo Final deprecation in Moodle 4.8 MDL-80956.
*/
class attempt_submitted {
/**
@ -35,8 +38,11 @@ class attempt_submitted {
*
* @param \mod_quiz\event\attempt_submitted $event
* @return void
* @deprecated Since Moodle 4.4 MDL-80099
*/
public static function process(\mod_quiz\event\attempt_submitted $event): void {
debugging('quiz_statistics\event\observer\attempt_submitted event observer has been deprecated in favour of ' .
'the quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted hook callback.', DEBUG_DEVELOPER);
$data = $event->get_data();
recalculate::queue_future_run($data['other']['quizid']);
}

View File

@ -16,7 +16,10 @@
namespace quiz_statistics;
use core\dml\sql_join;
use mod_quiz\hook\attempt_state_changed;
use mod_quiz\hook\structure_modified;
use mod_quiz\quiz_attempt;
use quiz_statistics\task\recalculate;
/**
* Hook callbacks
@ -47,4 +50,20 @@ class hook_callbacks {
$report = new \quiz_statistics_report();
$report->clear_cached_data($qubaids);
}
/**
* Queue a statistics recalculation when an attempt is submitted or deleting.
*
* @param attempt_state_changed $hook
* @return bool True if a task was queued.
*/
public static function quiz_attempt_submitted_or_deleted(attempt_state_changed $hook): bool {
$originalattempt = $hook->get_original_attempt();
$updatedattempt = $hook->get_updated_attempt();
if (is_null($updatedattempt) || $updatedattempt->state === quiz_attempt::FINISHED) {
// Only recalculate on deletion or submission.
return recalculate::queue_future_run($originalattempt->quiz);
}
return false;
}
}

View File

@ -25,6 +25,8 @@ use quiz_statistics\task\recalculate;
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Since Moodle 4.4 MDL-80099.
* @todo Final deprecation in Moodle 4.8 MDL-80956.
*/
class quiz_attempt_deleted {
/**
@ -32,8 +34,11 @@ class quiz_attempt_deleted {
*
* @param int $quizid The quiz the attempt belongs to.
* @return void
* @deprecated Since Moodle 4.4 MDL-80099.
*/
public static function callback(int $quizid): void {
debugging('quiz_statistics\quiz_attempt_deleted callback class has been deprecated in favour of ' .
'the quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted hook callback.', DEBUG_DEVELOPER);
recalculate::queue_future_run($quizid);
}
}

View File

@ -1,33 +0,0 @@
<?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/>.
/**
* Add event observers for quiz_statistics
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = [
[
'eventname' => '\mod_quiz\event\attempt_submitted',
'callback' => '\quiz_statistics\event\observer\attempt_submitted::process',
],
];

View File

@ -29,4 +29,9 @@ $callbacks = [
'callback' => quiz_statistics\hook_callbacks::class . '::quiz_structure_modified',
'priority' => 500,
],
[
'hook' => mod_quiz\hook\attempt_state_changed::class,
'callback' => quiz_statistics\hook_callbacks::class . '::quiz_attempt_submitted_or_deleted',
'priority' => 500,
],
];

View File

@ -32,7 +32,7 @@ use quiz_statistics\tests\statistics_test_trait;
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quiz_statistics\quiz_attempt_deleted
* @covers \quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted
*/
class quiz_attempt_deleted_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;

View File

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace quiz_statistics\event\observer;
namespace quiz_statistics;
defined('MOODLE_INTERNAL') || die();
@ -32,13 +32,12 @@ use quiz_statistics\tests\statistics_test_trait;
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quiz_statistics\event\observer\attempt_submitted
* @covers \quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted
*/
class attempt_submitted_test extends \advanced_testcase {
class quiz_attempt_submitted_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
use statistics_test_trait;
/**
* Attempting a quiz should queue the recalculation task for that quiz in 1 hour's time.
*

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2023100900;
$plugin->version = 2023100901;
$plugin->requires = 2023100400;
$plugin->component = 'quiz_statistics';