Skip to content

Commit

Permalink
Merge branch 'MDL-79254_master' of https://github.com/marxjohnson/moodle
Browse files Browse the repository at this point in the history
  • Loading branch information
junpataleta committed Oct 3, 2023
2 parents 9938ee3 + e5a7a18 commit 6ee9522
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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 quiz_statistics\event\observer;

use quiz_statistics\task\recalculate;

/**
* Event observer for \mod_quiz\event\attempt_submitted
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_submitted {
/**
* Queue an ad-hoc task to recalculate statistics for the quiz.
*
* This will defer running the task for 1 hour, to give other attempts in progress
* a chance to submit.
*
* @param \mod_quiz\event\attempt_submitted $event
* @return void
*/
public static function process(\mod_quiz\event\attempt_submitted $event): void {
$data = $event->get_data();
$quizid = $data['other']['quizid'];

$task = recalculate::instance($quizid);
$task->set_next_run_time(time() + HOURSECS);
\core\task\manager::queue_adhoc_task($task, true);
}
}
120 changes: 47 additions & 73 deletions mod/quiz/report/statistics/classes/task/recalculate.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

use core\dml\sql_join;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use quiz_statistics_report;

defined('MOODLE_INTERNAL') || die();
Expand All @@ -36,88 +35,63 @@
* @author Nathan Nguyen <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class recalculate extends \core\task\scheduled_task {
/** @var int the maximum length of time one instance of this task will run. */
const TIME_LIMIT = 3600;
class recalculate extends \core\task\adhoc_task {
/**
* Create a new instance of the task.
*
* This sets the properties so that only one task will be queued at a time for a given quiz.
*
* @param int $quizid
* @return recalculate
*/
public static function instance(int $quizid): recalculate {
$task = new self();
$task->set_component('quiz_statistics');
$task->set_userid(get_admin()->id);
$task->set_custom_data((object)[
'quizid' => $quizid,
]);
return $task;
}


public function get_name(): string {
return get_string('recalculatetask', 'quiz_statistics');
}

public function execute(): void {
global $DB;
$stoptime = time() + self::TIME_LIMIT;
$dateformat = get_string('strftimedatetimeshortaccurate', 'core_langconfig');
$data = $this->get_custom_data();
$quiz = $DB->get_record('quiz', ['id' => $data->quizid]);
if (!$quiz) {
mtrace('Could not find quiz with ID ' . $data->quizid . '.');
return;
}
$course = $DB->get_record('course', ['id' => $quiz->course]);
if (!$course) {
mtrace('Could not find course with ID ' . $quiz->course . '.');
return;
}
$attemptcount = $DB->count_records('quiz_attempts', ['quiz' => $data->quizid, 'state' => quiz_attempt::FINISHED]);
if ($attemptcount === 0) {
mtrace('Could not find any finished attempts for course with ID ' . $data->quizid . '.');
return;
}

// TODO: MDL-75197, add quizid in quiz_statistics so that it is simpler to find quizzes for stats calculation.
// Only calculate stats for quizzes which have recently finished attempt.
$latestattempts = $DB->get_records_sql("
SELECT q.id AS quizid,
q.name AS quizname,
q.grademethod AS quizgrademethod,
c.id AS courseid,
c.shortname AS courseshortname,
MAX(quiza.timefinish) AS mostrecentattempttime,
COUNT(1) AS numberofattempts
FROM {quiz_attempts} quiza
JOIN {quiz} q ON q.id = quiza.quiz
JOIN {course} c ON c.id = q.course
WHERE quiza.preview = 0
AND quiza.state = :quizstatefinished
GROUP BY q.id, q.name, q.grademethod, c.id, c.shortname
ORDER BY MAX(quiza.timefinish) DESC
", ["quizstatefinished" => quiz_attempt::FINISHED]);

$anyexception = null;
foreach ($latestattempts as $latestattempt) {
if (time() >= $stoptime) {
mtrace("This task has been running for more than " .
format_time(self::TIME_LIMIT) . " so stopping this execution.");
break;
}

// Check if there is any existing question stats, and it has been calculated after latest quiz attempt.
$qubaids = quiz_statistics_qubaids_condition($latestattempt->quizid,
new sql_join(), $latestattempt->quizgrademethod);
$lateststatstime = $DB->get_field('quiz_statistics', 'COALESCE(MAX(timemodified), 0)',
['hashcode' => $qubaids->get_hash_code()]);

$quizinfo = "'$latestattempt->quizname' ($latestattempt->quizid) in course " .
"$latestattempt->courseshortname ($latestattempt->courseid) has most recent attempt finished at " .
userdate($latestattempt->mostrecentattempttime, $dateformat);
if ($lateststatstime) {
$quizinfo .= " and statistics from " . userdate($lateststatstime, $dateformat);
}

if ($lateststatstime >= $latestattempt->mostrecentattempttime) {
mtrace(" " . $quizinfo . " so nothing to do.");
continue;
}

// OK, so we need to calculate for this quiz.
mtrace(" " . $quizinfo . " so re-calculating statistics for $latestattempt->numberofattempts attempts, start time " .
userdate(time(), $dateformat) . " ...");

try {
$quizobj = quiz_settings::create($latestattempt->quizid);
$report = new quiz_statistics_report();
$report->clear_cached_data($qubaids);
$report->calculate_questions_stats_for_question_bank($quizobj->get_quizid());
mtrace(" Calculations completed at " . userdate(time(), $dateformat) . ".");
mtrace("Re-calculating statistics for quiz {$quiz->name} ({$quiz->id}) " .
"from course {$course->shortname} ({$course->id}) with {$attemptcount} attempts, start time " .
userdate(time(), $dateformat) . " ...");

} catch (\Throwable $e) {
// We don't want an exception from one quiz to stop processing of other quizzes.
mtrace_exception($e);
$anyexception = $e;
}
}
$qubaids = quiz_statistics_qubaids_condition(
$quiz->id,
new sql_join(),
$quiz->grademethod
);

if ($anyexception) {
// If there was any error, ensure the task fails.
throw $anyexception;
}
$report = new quiz_statistics_report();
$report->clear_cached_data($qubaids);
$report->calculate_questions_stats_for_question_bank($quiz->id);
mtrace(' Calculations completed at ' . userdate(time(), $dateformat) . '.');
}
}
46 changes: 46 additions & 0 deletions mod/quiz/report/statistics/classes/tests/statistics_helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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 quiz_statistics\tests;

/**
* Test helper functions for statistics
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statistics_helper {
/**
* Run any ad-hoc recalculation tasks that have been scheduled.
*
* We need a special function to do this as the tasks are deferred by one hour,
* so we need to pass a custom $timestart argument.
*
* @return void
*/
public static function run_pending_recalculation_tasks(): void {
while ($task = \core\task\manager::get_next_adhoc_task(
time() + HOURSECS + 1,
false,
'\quiz_statistics\task\recalculate'
)) {
$task->execute();
\core\task\manager::adhoc_task_complete($task);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,19 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Legacy Cron Quiz Reports Task
*
* @package quiz_statistics
* @copyright 2017 Michael Hughes, University of Strathclyde
* @author Michael Hughes <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* Add event observers for quiz_statistics
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

defined('MOODLE_INTERNAL') || die();

$tasks = [
$observers = [
[
'classname' => 'quiz_statistics\task\recalculate',
'blocking' => 0,
'minute' => 'R',
'hour' => '*/4',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
]
'eventname' => '\mod_quiz\event\attempt_submitted',
'callback' => '\quiz_statistics\event\observer\attempt_submitted::process',
],
];
Loading

0 comments on commit 6ee9522

Please sign in to comment.