Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix catquizstatistics shortcode on non-course pages #685

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions classes/local/status.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ class status {
*/
const EXCEEDED_MAX_ATTEMPT_TIME = 'exceededmaxattempttime';

/**
* Indicates that the attempt was closed automatically.
*
* @var string
*/
const CLOSED_BY_TIMELIMIT = 'attemptclosedbytimelimit';

/**
* An undefined status
*
Expand All @@ -124,6 +131,7 @@ class status {
self::ERROR_EMPTY_FIRST_QUESTION_LIST => 6,
self::ERROR_NO_ITEMS => 7,
self::EXCEEDED_MAX_ATTEMPT_TIME => 8,
self::CLOSED_BY_TIMELIMIT => 9,
];

/**
Expand Down
191 changes: 191 additions & 0 deletions classes/task/cancel_expired_attempts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?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/>.

/**
* Class cancel_expired_attempts.
*
* @package local_catquiz
* @author David Szkiba <[email protected]>
* @copyright 2024 Wunderbyte GmbH
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace local_catquiz\task;

use cache_helper;
use context_module;
use dml_exception;
use local_catquiz\catquiz;
use local_catquiz\local\status;
use mod_adaptivequiz\local\attempt\attempt;
use mod_adaptivequiz\local\attempt\attempt_state;
use stdClass;

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

global $CFG;
require_once("$CFG->dirroot/mod/adaptivequiz/locallib.php");

/**
* Cancels open CAT quiz attempts that exceeded the timeout.
*
* @package local_catquiz
* @author David Szkiba <[email protected]>
* @copyright 2024 Wunderbyte GmbH
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cancel_expired_attempts extends \core\task\scheduled_task {

/**
* Holds adaptivequiz records as stdClass entires.
*
* @var array
*/
private array $quizzes = [];

/**
* Holds the local_catquiz_tests of open attempts.
*
* @var array
*/
private array $maxtimepertest = [];
/**
* Returns task name.
* @return string
*/
public function get_name() {
return get_string('cancelexpiredattempts', 'local_catquiz');
}

/**
* Cancel expired quiz attempts.
*
* @return void
*/
public function execute() {
global $DB;
mtrace("Running cancel_expired_attempts task.");

// Get all catquiz attempts that are still in progress.
$sql = <<<SQL
SELECT aa.*
FROM {adaptivequiz} a
JOIN {adaptivequiz_attempt} aa
ON a.id = aa.instance
WHERE a.catmodel = 'catquiz'
AND aa.attemptstate = :inprogress
SQL;
$params = ['inprogress' => attempt_state::IN_PROGRESS];
if (!$records = $DB->get_records_sql($sql, $params)) {
mtrace("No attempts are in progress. Exiting.");
return;
}

// Get all local_catquiz_tests records that are used by open attempts.
$openinstances = array_unique(
array_map(
fn($r) => $r->instance,
$records
)
);
// For each test, get the maximum time per attempt setting.
foreach ($DB->get_records_list('local_catquiz_tests', 'componentid', $openinstances) as $tr) {
$settings = json_decode($tr->json);
// If this setting is not given, it is not limited on the quiz level.
if (
!property_exists($settings, 'catquiz_timelimitgroup')
|| !$settings->catquiz_timelimitgroup
) {
$this->maxtimepertest[$tr->componentid] = null;
}

// The max time per attempt can be given in minutes or hours. We convert it to seconds to
// compare it to the current time.
$maxtimeperattempt = $settings->catquiz_timelimitgroup->catquiz_maxtimeperattempt * 60;
if ($settings->catquiz_timelimitgroup->catquiz_timeselect_attempt == 'h') {
$maxtimeperattempt *= 60;
}
$this->maxtimepertest[$tr->componentid] = $maxtimeperattempt;
}

// For each record, check if the attempt is running longer than the default maximum time or the
// maximum time defined by the quiz. If so, mark it as completed with the exceeded threshold state.
$now = time();
$defaultmaxtime = 60 * 60 * get_config('local_catquiz', 'maximum_attempt_duration_hours');
$completed = 0;
$statusmessage = get_string('attemptclosedbytimelimit', 'local_catquiz');
foreach ($records as $record) {
// If it is set on a quiz setting basis and not triggered, ignore the default setting.
$quizmaxtime = $this->maxtimepertest[$record->instance];
$exceedsmaxtime = $this->exceeds_maxtime($record->timecreated, $quizmaxtime, $defaultmaxtime, $now);
if ($exceedsmaxtime) {
$attempt = attempt::get_by_id($record->id);
$quiz = $this->get_adaptivequiz($record->instance);
$cm = get_coursemodule_from_instance('adaptivequiz', $record->instance);
$context = context_module::instance($cm->id);
$attempt->complete($quiz, $context, $statusmessage, $now);
catquiz::set_final_attempt_status($record->id, status::CLOSED_BY_TIMELIMIT);
cache_helper::purge_by_event('changesinquizattempts');
$completed++;
}
}
$duration = time() - $now;
mtrace(sprintf(
'Processed %d open attempts in %d seconds and marked %d as completed',
count($records),
$duration,
$completed
));
}

/**
* Checks whether the given attempt exceeds the max attempt time
*
* @param int $timecreated Timestamp when the attempt was created
* @param ?int $quizmaxtime Maximum time specified by the quiz
* @param int $defaultmaxtime Default maximum time
* @param int $now
* @return bool
*/
public function exceeds_maxtime(int $timecreated, ?int $quizmaxtime, int $defaultmaxtime, int $now): bool {
// Get the timeout that should be used.
// If a timeout is set per quiz, use this. If not, fall back to the global default.
$maxtime = $quizmaxtime ?? $defaultmaxtime;
// The value 0 is treated as "no limit".
if ($maxtime === 0) {
return false;
}
return $now - $timecreated > $maxtime;
}

/**
* Returns an adaptivequiz with the given ID.
*
* @param int $id
* @return stdClass
* @throws dml_exception
*/
private function get_adaptivequiz(int $id): stdClass {
global $DB;
if (array_key_exists($id, $this->quizzes)) {
return $this->quizzes[$id];
}

$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $id], '*', MUST_EXIST);
$this->quizzes[$adaptivequiz->id] = $adaptivequiz;
return $adaptivequiz;
}
}
10 changes: 10 additions & 0 deletions db/tasks.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

use local_catquiz\task\cancel_expired_attempts;
use local_catquiz\task\recalculate_cat_model_params;

defined('MOODLE_INTERNAL') || die();
Expand All @@ -36,4 +37,13 @@
'dayofweek' => '*',
'month' => '*',
],
[
'classname' => cancel_expired_attempts::class,
'blocking' => 0,
'minute' => '*/5', // Runs every 5 minutes.
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*',
],
];
4 changes: 4 additions & 0 deletions lang/de/local_catquiz.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
$string['assigntestitemstocatscales'] = 'Weise den Skalen Fragen zu';
$string['attempt_completed'] = 'Testversuch abgeschlossen';
$string['attemptchartstitle'] = 'Anzahl und Ergebnisse der Testversuche für Skala „{$a}“';
$string['attemptclosedbytimelimit'] = 'Versuch wurde wegen Zeitüberschreitung automatisch beendet';
$string['attemptfeedbacknotavailable'] = 'Kein Feedback verfügbar';
$string['attemptfeedbacknotyetavailable'] = 'Das Feedback wird angezeigt, sobald der laufende Versuch beendet ist.';
$string['attempts'] = 'Testversuche';
Expand Down Expand Up @@ -95,6 +96,7 @@
$string['callbackfunctionnotapplied'] = 'Callback Funktion konnte nicht angewandt werden.';
$string['callbackfunctionnotdefined'] = 'Callback Funktion nicht definiert.';
$string['canbesetto0iflabelgiven'] = 'Kann 0 sein, wenn Abgleich über Label stattfindet.';
$string['cancelexpiredattempts'] = 'Abgelaufene Versuche schließen';
$string['cannotdeletescalewithchildren'] = 'Skalen mit Unterskalen können nicht gelöscht werden.';
$string['catcatscaleprime'] = 'Inhaltsbereich (Globalskala)';
$string['catcatscaleprime_help'] = 'Wählen Sie den für Sie relevanten Inhaltsbereich aus. Inhaltsbereche werden als Skala durch eine*n CAT-Manager*in angelegt und verwaltet. Falls Sie eigene Inhalts- und Unterbereiche wünschen, wenden Sie sich bitte an den oder die CAT-Manager*in oder den bzw. die Adminstrator*in Ihrer Moodle-Instanz.';
Expand Down Expand Up @@ -439,6 +441,8 @@
$string['max_iterations'] = 'Maximale Anzahl an Iterationen';
$string['maxabilityscalevalue'] = 'Maximale Personenfähigkeit:';
$string['maxabilityscalevalue_help'] = 'Geben Sie die größtmögliche Personenfähigkeit dieser Skala als Dezimalwert an. Der Mittelwert ist null.';
$string['maxattemptduration'] = 'Maximale Laufzeit für Versuche';
$string['maxattemptduration_desc'] = 'Versuche die älter sind werden automatisch geschlossen. Ein Wert von 0 bedeutet, dass die Laufzeit unbeschränkt ist. Dieser Wert kann in den Quiz-Einstellungen überschrieben werden.';
$string['maxquestionspersubscale'] = 'max. Frageanzahl pro Skala';
$string['maxquestionspersubscale_help'] = 'Wenn von einer Skala so viele Fragen angezeigt wurden, werden keine weiteren Fragen dieser Skala mehr ausgespielt. Wenn auf 0 gesetzt, dann gibt es kein Limit.';
$string['maxscalevalue'] = 'Maximalwert';
Expand Down
4 changes: 4 additions & 0 deletions lang/en/local_catquiz.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
$string['assigntestitemstocatscales'] = 'Assign testitem to CAT scale';
$string['attempt_completed'] = 'Attempt completed';
$string['attemptchartstitle'] = 'Number and results of attempts in scale “{$a}”';
$string['attemptclosedbytimelimit'] = 'Attempt automatically closed due to exceeded time limit';
$string['attemptfeedbacknotavailable'] = 'No feedback available';
$string['attemptfeedbacknotyetavailable'] = 'Feedback for attempts will be displayed when available.';
$string['attempts'] = 'Attempts';
Expand Down Expand Up @@ -95,6 +96,7 @@
$string['callbackfunctionnotapplied'] = 'Callback function could not be applied.';
$string['callbackfunctionnotdefined'] = 'Callback function is not defined.';
$string['canbesetto0iflabelgiven'] = 'Can be 0 if matching of testitem is via label.';
$string['cancelexpiredattempts'] = 'Cancel attempts exceeding maximum time';
$string['cannotdeletescalewithchildren'] = 'Cannot delete CAT scale with children';
$string['catcatscaleprime'] = 'Content/Scale';
$string['catcatscaleprime_help'] = 'Select the content area that is relevant to you. Content areas are created and managed as a so-called scale by a CAT manager. If you would like your own content and sub-areas, please contact the CAT manager or the administrator of your Moodle instance.';
Expand Down Expand Up @@ -420,6 +422,8 @@
$string['max_iterations'] = 'Maximum number of iterations';
$string['maxabilityscalevalue'] = 'Person ability maximum:';
$string['maxabilityscalevalue_help'] = 'Enter the highest possible person ability of this scale as a positive decimal value. The mean is zero.';
$string['maxattemptduration'] = 'The maximum duration of attempts';
$string['maxattemptduration_desc'] = 'Attempts older than this will be marked as completed. A value of 0 means that there is no limit. This value can be overwritten by the quiz settings.';
$string['maxquestionspersubscale'] = 'Maximum number of questions returned per subscale';
$string['maxquestionspersubscale_help'] = 'When this number of questions was returned for any subscale, no more questions from this scale will be shown. A value of 0 means that there is no limit.';
$string['maxscalevalue'] = 'Max value';
Expand Down
9 changes: 9 additions & 0 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,13 @@
get_string('store_debug_info_name', 'local_catquiz'),
get_string('store_debug_info_desc', 'local_catquiz'),
0));

// Add a setting for the default maximum attempt duration.
$settings->add(new admin_setting_configtext(
'local_catquiz/maximum_attempt_duration_hours',
get_string('maxattemptduration', 'local_catquiz'),
get_string('maxattemptduration_desc', 'local_catquiz'),
24, // Default value.
PARAM_INT // Expect integer type.
));
}
Loading
Loading