Skip to content

Commit

Permalink
GH-620 Close open attempts
Browse files Browse the repository at this point in the history
  • Loading branch information
davidszkiba committed Oct 1, 2024
1 parent 37e0037 commit 3eb017a
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 1 deletion.
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
188 changes: 188 additions & 0 deletions classes/task/cancel_expired_attempts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?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;

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
* @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

0 comments on commit 3eb017a

Please sign in to comment.