mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 22:08:20 +01:00
Merge branch 'MDL-35745' of https://github.com/timhunt/moodle
This commit is contained in:
commit
ecc0f661c5
16
mod/quiz/amd/build/reopen_attempt_ui.min.js
vendored
Normal file
16
mod/quiz/amd/build/reopen_attempt_ui.min.js
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
define("mod_quiz/reopen_attempt_ui",["exports","core/notification","core/ajax","core/str"],(function(_exports,_notification,_ajax,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
|
||||
/**
|
||||
* This module has the code to make the Re-open attempt button work, if present.
|
||||
*
|
||||
* That is, it looks for buttons with HTML like
|
||||
* <button type="button" data-action="reopen-attempt" data-attempt-id="227000" data-after-action-url="/mod/quiz/report.php">
|
||||
* and if that is clicked, it first shows an 'Are you sure' pop-up, and if they are sure,
|
||||
* the attempt is re-opened, and then the page reloads.
|
||||
*
|
||||
* @module mod_quiz/reopen_attempt_ui
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
const reopenButtonClicked=async e=>{if(!(e.target instanceof HTMLElement&&e.target.matches('button[data-action="reopen-attempt"]')))return;e.preventDefault();const attemptId=e.target.dataset.attemptId;try{const messages=(0,_ajax.call)([{methodname:"mod_quiz_get_reopen_attempt_confirmation",args:{attemptid:attemptId}}]);await(0,_notification.saveCancelPromise)((0,_str.get_string)("reopenattemptareyousuretitle","mod_quiz"),messages[0],(0,_str.get_string)("reopenattempt","mod_quiz"),{triggerElement:e.target}),await(0,_ajax.call)([{methodname:"mod_quiz_reopen_attempt",args:{attemptid:attemptId}}])[0],window.location=M.cfg.wwwroot+e.target.dataset.afterActionUrl}catch(error){if("modal-save-cancel:cancel"===error.type)return;await(0,_notification.exception)(error)}};_exports.init=()=>{document.addEventListener("click",reopenButtonClicked)}}));
|
||||
|
||||
//# sourceMappingURL=reopen_attempt_ui.min.js.map
|
1
mod/quiz/amd/build/reopen_attempt_ui.min.js.map
Normal file
1
mod/quiz/amd/build/reopen_attempt_ui.min.js.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"reopen_attempt_ui.min.js","sources":["../src/reopen_attempt_ui.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * This module has the code to make the Re-open attempt button work, if present.\n *\n * That is, it looks for buttons with HTML like\n * <button type=\"button\" data-action=\"reopen-attempt\" data-attempt-id=\"227000\" data-after-action-url=\"/mod/quiz/report.php\">\n * and if that is clicked, it first shows an 'Are you sure' pop-up, and if they are sure,\n * the attempt is re-opened, and then the page reloads.\n *\n * @module mod_quiz/reopen_attempt_ui\n * @copyright 2023 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {exception as displayException} from 'core/notification';\nimport {call as fetchMany} from 'core/ajax';\nimport {get_string as getString} from 'core/str';\nimport {saveCancelPromise} from 'core/notification';\n\n/**\n * Handle a click if it is on one of our buttons.\n *\n * @param {MouseEvent} e the click event.\n */\nconst reopenButtonClicked = async(e) => {\n if (!(e.target instanceof HTMLElement) || !e.target.matches('button[data-action=\"reopen-attempt\"]')) {\n return;\n }\n\n e.preventDefault();\n const attemptId = e.target.dataset.attemptId;\n\n try {\n // We fetch the confirmation message from the server now, so the message is based\n // on the latest state of the attempt, rather than when the containing page loaded.\n const messages = fetchMany([{\n methodname: 'mod_quiz_get_reopen_attempt_confirmation',\n args: {\n \"attemptid\": attemptId,\n },\n }]);\n\n await saveCancelPromise(\n getString('reopenattemptareyousuretitle', 'mod_quiz'),\n messages[0],\n getString('reopenattempt', 'mod_quiz'),\n {triggerElement: e.target},\n );\n\n await (fetchMany([{\n methodname: 'mod_quiz_reopen_attempt',\n args: {\n \"attemptid\": attemptId,\n },\n }])[0]);\n window.location = M.cfg.wwwroot + e.target.dataset.afterActionUrl;\n\n } catch (error) {\n if (error.type === 'modal-save-cancel:cancel') {\n // User clicked Cancel, so do nothing.\n return;\n }\n await displayException(error);\n }\n};\n\nexport const init = () => {\n document.addEventListener('click', reopenButtonClicked);\n};\n"],"names":["reopenButtonClicked","async","e","target","HTMLElement","matches","preventDefault","attemptId","dataset","messages","methodname","args","triggerElement","window","location","M","cfg","wwwroot","afterActionUrl","error","type","document","addEventListener"],"mappings":";;;;;;;;;;;;;MAsCMA,oBAAsBC,MAAAA,SAClBC,EAAEC,kBAAkBC,aAAiBF,EAAEC,OAAOE,QAAQ,gDAI5DH,EAAEI,uBACIC,UAAYL,EAAEC,OAAOK,QAAQD,oBAKzBE,UAAW,cAAU,CAAC,CACxBC,WAAY,2CACZC,KAAM,WACWJ,oBAIf,oCACF,mBAAU,+BAAgC,YAC1CE,SAAS,IACT,mBAAU,gBAAiB,YAC3B,CAACG,eAAgBV,EAAEC,eAGhB,cAAU,CAAC,CACdO,WAAY,0BACZC,KAAM,WACWJ,cAEjB,GACJM,OAAOC,SAAWC,EAAEC,IAAIC,QAAUf,EAAEC,OAAOK,QAAQU,eAErD,MAAOC,UACc,6BAAfA,MAAMC,kBAIJ,2BAAiBD,uBAIX,KAChBE,SAASC,iBAAiB,QAAStB"}
|
83
mod/quiz/amd/src/reopen_attempt_ui.js
Normal file
83
mod/quiz/amd/src/reopen_attempt_ui.js
Normal file
@ -0,0 +1,83 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* This module has the code to make the Re-open attempt button work, if present.
|
||||
*
|
||||
* That is, it looks for buttons with HTML like
|
||||
* <button type="button" data-action="reopen-attempt" data-attempt-id="227000" data-after-action-url="/mod/quiz/report.php">
|
||||
* and if that is clicked, it first shows an 'Are you sure' pop-up, and if they are sure,
|
||||
* the attempt is re-opened, and then the page reloads.
|
||||
*
|
||||
* @module mod_quiz/reopen_attempt_ui
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {exception as displayException} from 'core/notification';
|
||||
import {call as fetchMany} from 'core/ajax';
|
||||
import {get_string as getString} from 'core/str';
|
||||
import {saveCancelPromise} from 'core/notification';
|
||||
|
||||
/**
|
||||
* Handle a click if it is on one of our buttons.
|
||||
*
|
||||
* @param {MouseEvent} e the click event.
|
||||
*/
|
||||
const reopenButtonClicked = async(e) => {
|
||||
if (!(e.target instanceof HTMLElement) || !e.target.matches('button[data-action="reopen-attempt"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const attemptId = e.target.dataset.attemptId;
|
||||
|
||||
try {
|
||||
// We fetch the confirmation message from the server now, so the message is based
|
||||
// on the latest state of the attempt, rather than when the containing page loaded.
|
||||
const messages = fetchMany([{
|
||||
methodname: 'mod_quiz_get_reopen_attempt_confirmation',
|
||||
args: {
|
||||
"attemptid": attemptId,
|
||||
},
|
||||
}]);
|
||||
|
||||
await saveCancelPromise(
|
||||
getString('reopenattemptareyousuretitle', 'mod_quiz'),
|
||||
messages[0],
|
||||
getString('reopenattempt', 'mod_quiz'),
|
||||
{triggerElement: e.target},
|
||||
);
|
||||
|
||||
await (fetchMany([{
|
||||
methodname: 'mod_quiz_reopen_attempt',
|
||||
args: {
|
||||
"attemptid": attemptId,
|
||||
},
|
||||
}])[0]);
|
||||
window.location = M.cfg.wwwroot + e.target.dataset.afterActionUrl;
|
||||
|
||||
} catch (error) {
|
||||
if (error.type === 'modal-save-cancel:cancel') {
|
||||
// User clicked Cancel, so do nothing.
|
||||
return;
|
||||
}
|
||||
await displayException(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const init = () => {
|
||||
document.addEventListener('click', reopenButtonClicked);
|
||||
};
|
@ -19,9 +19,9 @@ namespace mod_quiz;
|
||||
use core_component;
|
||||
use mod_quiz\form\preflight_check_form;
|
||||
use mod_quiz\local\access_rule_base;
|
||||
use mod_quiz\output\renderer;
|
||||
use mod_quiz\question\display_options;
|
||||
use mod_quiz_mod_form;
|
||||
use mod_quiz\output\renderer;
|
||||
use moodle_page;
|
||||
use moodle_url;
|
||||
use MoodleQuickForm;
|
||||
|
81
mod/quiz/classes/event/attempt_reopened.php
Normal file
81
mod/quiz/classes/event/attempt_reopened.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?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\event;
|
||||
|
||||
use coding_exception;
|
||||
use core\event\base;
|
||||
use moodle_url;
|
||||
|
||||
/**
|
||||
* Event fired when a quiz attempt is reopened.
|
||||
*
|
||||
* @property-read array $other {
|
||||
* Extra information about event.
|
||||
*
|
||||
* - int submitterid: id of submitter (null when triggered by CLI script).
|
||||
* - int quizid: (optional) id of the quiz.
|
||||
* }
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @since Moodle 4.2
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class attempt_reopened extends base {
|
||||
|
||||
protected function init() {
|
||||
$this->data['objecttable'] = 'quiz_attempts';
|
||||
$this->data['crud'] = 'u';
|
||||
$this->data['edulevel'] = self::LEVEL_TEACHING;
|
||||
}
|
||||
|
||||
public function get_description(): string {
|
||||
return "The user with id '$this->relateduserid' has had their attempt with id '$this->objectid'" .
|
||||
"for the quiz with course module id '$this->contextinstanceid' re-opened by the user with id '$this->userid'.";
|
||||
}
|
||||
|
||||
public static function get_name(): string {
|
||||
return get_string('eventquizattemptreopened', 'mod_quiz');
|
||||
}
|
||||
|
||||
public function get_url(): moodle_url {
|
||||
return new moodle_url('/mod/quiz/review.php', ['attempt' => $this->objectid]);
|
||||
}
|
||||
|
||||
protected function validate_data(): void {
|
||||
parent::validate_data();
|
||||
|
||||
if (!isset($this->relateduserid)) {
|
||||
throw new coding_exception('The \'relateduserid\' must be set.');
|
||||
}
|
||||
|
||||
if (!array_key_exists('submitterid', $this->other)) {
|
||||
throw new coding_exception('The \'submitterid\' value must be set in other.');
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_objectid_mapping(): array {
|
||||
return ['db' => 'quiz_attempts', 'restore' => 'quiz_attempt'];
|
||||
}
|
||||
|
||||
public static function get_other_mapping(): array {
|
||||
return [
|
||||
'submitterid' => ['db' => 'user', 'restore' => 'user'],
|
||||
'quizid' => ['db' => 'quiz', 'restore' => 'quiz'],
|
||||
];
|
||||
}
|
||||
}
|
98
mod/quiz/classes/external/get_reopen_attempt_confirmation.php
vendored
Normal file
98
mod/quiz/classes/external/get_reopen_attempt_confirmation.php
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
<?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\external;
|
||||
|
||||
use core_external\external_api;
|
||||
use core_external\external_description;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use Exception;
|
||||
use html_writer;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use moodle_exception;
|
||||
|
||||
/**
|
||||
* Web service to check a quiz attempt state, and return a confirmation message if it can be reopened now.
|
||||
*
|
||||
* The use must have the 'mod/quiz:reopenattempts' capability and the attempt
|
||||
* must (at least for now) be in the 'Never submitted' state (quiz_attempt::ABANDONED).
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class get_reopen_attempt_confirmation extends external_api {
|
||||
|
||||
/**
|
||||
* Declare the method parameters.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'attemptid' => new external_value(PARAM_INT, 'The id of the attempt to reopen'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a quiz attempt state, and return a confirmation message method implementation.
|
||||
*
|
||||
* @param int $attemptid the id of the attempt to reopen.
|
||||
* @return string a suitable confirmation message (HTML), if the attempt is suitable to be reopened.
|
||||
* @throws Exception an appropriate exception if the attempt cannot be reopened now.
|
||||
*/
|
||||
public static function execute(int $attemptid): string {
|
||||
global $DB;
|
||||
['attemptid' => $attemptid] = self::validate_parameters(
|
||||
self::execute_parameters(), ['attemptid' => $attemptid]);
|
||||
|
||||
// Check the request is valid.
|
||||
$attemptobj = quiz_attempt::create($attemptid);
|
||||
require_capability('mod/quiz:reopenattempts', $attemptobj->get_context());
|
||||
self::validate_context($attemptobj->get_context());
|
||||
if ($attemptobj->get_state() != quiz_attempt::ABANDONED) {
|
||||
throw new moodle_exception('reopenattemptwrongstate', 'quiz', '',
|
||||
['attemptid' => $attemptid, 'state' => quiz_attempt_state_name($attemptobj->get_state())]);
|
||||
}
|
||||
|
||||
// Work out what the affect or re-opening will be.
|
||||
$timestamp = time();
|
||||
$timeclose = $attemptobj->get_access_manager(time())->get_end_time($attemptobj->get_attempt());
|
||||
if ($timeclose && $timestamp > $timeclose) {
|
||||
$expectedoutcome = get_string('reopenedattemptwillbesubmitted', 'quiz');
|
||||
} else if ($timeclose) {
|
||||
$expectedoutcome = get_string('reopenedattemptwillbeinprogressuntil', 'quiz', userdate($timeclose));
|
||||
} else {
|
||||
$expectedoutcome = get_string('reopenedattemptwillbeinprogress', 'quiz');
|
||||
}
|
||||
|
||||
// Return the required message.
|
||||
$user = $DB->get_record('user', ['id' => $attemptobj->get_userid()], '*', MUST_EXIST);
|
||||
return html_writer::tag('p', get_string('reopenattemptareyousuremessage', 'quiz',
|
||||
['attemptnumber' => $attemptobj->get_attempt_number(), 'attemptuser' => s(fullname($user))])) .
|
||||
html_writer::tag('p', $expectedoutcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the webservice response.
|
||||
*
|
||||
* @return external_description
|
||||
*/
|
||||
public static function execute_returns(): external_description {
|
||||
return new external_value(PARAM_RAW, 'Confirmation to show the user before the attempt is reopened.');
|
||||
}
|
||||
}
|
79
mod/quiz/classes/external/reopen_attempt.php
vendored
Normal file
79
mod/quiz/classes/external/reopen_attempt.php
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
<?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\external;
|
||||
|
||||
use core_external\external_api;
|
||||
use core_external\external_description;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use moodle_exception;
|
||||
|
||||
/**
|
||||
* Web service method for re-opening a quiz attempt.
|
||||
*
|
||||
* The use must have the 'mod/quiz:reopenattempts' capability and the attempt
|
||||
* must (at least for now) be in the 'Never submitted' state (quiz_attempt::ABANDONED).
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class reopen_attempt extends external_api {
|
||||
|
||||
/**
|
||||
* Declare the method parameters.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'attemptid' => new external_value(PARAM_INT, 'The id of the attempt to reopen'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-opening a submitted attempt method implementation.
|
||||
*
|
||||
* @param int $attemptid the id of the attempt to reopen.
|
||||
*/
|
||||
public static function execute(int $attemptid): void {
|
||||
['attemptid' => $attemptid] = self::validate_parameters(
|
||||
self::execute_parameters(), ['attemptid' => $attemptid]);
|
||||
|
||||
// Check the request is valid.
|
||||
$attemptobj = quiz_attempt::create($attemptid);
|
||||
require_capability('mod/quiz:reopenattempts', $attemptobj->get_context());
|
||||
self::validate_context($attemptobj->get_context());
|
||||
if ($attemptobj->get_state() != quiz_attempt::ABANDONED) {
|
||||
throw new moodle_exception('reopenattemptwrongstate', 'quiz', '',
|
||||
['attemptid' => $attemptid, 'state' => quiz_attempt_state_name($attemptobj->get_state())]);
|
||||
}
|
||||
|
||||
// Re-open the attempt.
|
||||
$attemptobj->process_reopen_abandoned(time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the webservice response.
|
||||
*
|
||||
* @return external_description|null always null.
|
||||
*/
|
||||
public static function execute_returns(): ?external_description {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -246,8 +246,10 @@ abstract class attempts_report extends report_base {
|
||||
* @param array $headers the columns headings. Added to.
|
||||
*/
|
||||
protected function add_state_column(&$columns, &$headers) {
|
||||
global $PAGE;
|
||||
$columns[] = 'state';
|
||||
$headers[] = get_string('attemptstate', 'quiz');
|
||||
$PAGE->requires->js_call_amd('mod_quiz/reopen_attempt_ui', 'init');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -87,6 +87,9 @@ abstract class attempts_report_table extends \table_sql {
|
||||
/** @var string strftime format. */
|
||||
protected $strtimeformat;
|
||||
|
||||
/** @var bool|null used by {@see col_state()} to cache the has_capability result. */
|
||||
protected $canreopen = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -177,11 +180,27 @@ abstract class attempts_report_table extends \table_sql {
|
||||
* @return string HTML content to go inside the td.
|
||||
*/
|
||||
public function col_state($attempt) {
|
||||
if (!is_null($attempt->attempt)) {
|
||||
return quiz_attempt::state_name($attempt->state);
|
||||
} else {
|
||||
return '-';
|
||||
if (is_null($attempt->attempt)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$display = quiz_attempt::state_name($attempt->state);
|
||||
if ($this->is_downloading()) {
|
||||
return $display;
|
||||
}
|
||||
|
||||
$this->canreopen ??= has_capability('mod/quiz:reopenattempts', $this->context);
|
||||
if ($attempt->state == quiz_attempt::ABANDONED && $this->canreopen) {
|
||||
$display .= ' ' . html_writer::tag('button', get_string('reopenattempt', 'quiz'), [
|
||||
'type' => 'button',
|
||||
'class' => 'btn btn-secondary',
|
||||
'data-action' => 'reopen-attempt',
|
||||
'data-attempt-id' => $attempt->attempt,
|
||||
'data-after-action-url' => $this->reporturl->out_as_local_url(false),
|
||||
]);
|
||||
}
|
||||
|
||||
return $display;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1846,6 +1846,39 @@ class quiz_attempt {
|
||||
$transaction->allow_commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes an attempt in the 'Never submitted' state, and reopens it.
|
||||
*
|
||||
* If, for this student, time has not expired (perhaps, because an override has
|
||||
* been added, then the attempt is left open. Otherwise, it is immediately submitted
|
||||
* for grading.
|
||||
*
|
||||
* @param int $timestamp the time to deem as now.
|
||||
*/
|
||||
public function process_reopen_abandoned($timestamp) {
|
||||
global $DB;
|
||||
|
||||
// Verify that things are as we expect.
|
||||
if ($this->get_state() != self::ABANDONED) {
|
||||
throw new coding_exception('Can only reopen an attempt that was never submitted.');
|
||||
}
|
||||
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
$this->attempt->timemodified = $timestamp;
|
||||
$this->attempt->state = self::IN_PROGRESS;
|
||||
$this->attempt->timecheckstate = null;
|
||||
$DB->update_record('quiz_attempts', $this->attempt);
|
||||
|
||||
$this->fire_state_transition_event('\mod_quiz\event\attempt_reopened', $timestamp, false);
|
||||
|
||||
$timeclose = $this->get_access_manager($timestamp)->get_end_time($this->attempt);
|
||||
if ($timeclose && $timestamp > $timeclose) {
|
||||
$this->process_finish($timestamp, false, $timeclose);
|
||||
}
|
||||
|
||||
$transaction->allow_commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a state transition event.
|
||||
*
|
||||
|
@ -165,6 +165,16 @@ $capabilities = [
|
||||
]
|
||||
],
|
||||
|
||||
// Re-open attempts after they are closed.
|
||||
'mod/quiz:reopenattempts' => [
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'archetypes' => [
|
||||
'editingteacher' => CAP_ALLOW,
|
||||
'manager' => CAP_ALLOW
|
||||
]
|
||||
],
|
||||
|
||||
// Do not have the time limit imposed. Used for accessibility legislation compliance.
|
||||
'mod/quiz:ignoretimelimits' => [
|
||||
'captype' => 'read',
|
||||
|
@ -199,4 +199,20 @@ $functions = [
|
||||
'capabilities' => 'mod/quiz:view',
|
||||
'ajax' => true,
|
||||
],
|
||||
|
||||
'mod_quiz_reopen_attempt' => [
|
||||
'classname' => 'mod_quiz\external\reopen_attempt',
|
||||
'description' => 'Re-open an attempt that is currently in the never submitted state.',
|
||||
'type' => 'write',
|
||||
'capabilities' => 'mod/quiz:reopenattempts',
|
||||
'ajax' => true,
|
||||
],
|
||||
|
||||
'mod_quiz_get_reopen_attempt_confirmation' => [
|
||||
'classname' => 'mod_quiz\external\get_reopen_attempt_confirmation',
|
||||
'description' => 'Verify it is OK to re-open a given quiz attempt, and if so, return a suitable confirmation message.',
|
||||
'type' => 'read',
|
||||
'capabilities' => 'mod/quiz:reopenattempts',
|
||||
'ajax' => true,
|
||||
],
|
||||
];
|
||||
|
@ -370,10 +370,11 @@ $string['eventpagebreakcreated'] = 'Page break created';
|
||||
$string['eventpagebreakdeleted'] = 'Page break deleted';
|
||||
$string['eventquestionmanuallygraded'] = 'Question manually graded';
|
||||
$string['eventquizattemptabandoned'] = 'Quiz attempt abandoned';
|
||||
$string['eventquizattempttimelimitexceeded'] = 'Quiz attempt time limit exceeded';
|
||||
$string['eventquizattemptregraded'] = 'Quiz attempt regraded';
|
||||
$string['eventquizattemptreopened'] = 'Quiz attempt re-openend';
|
||||
$string['eventquizattemptstarted'] = 'Quiz attempt started';
|
||||
$string['eventquizattemptsubmitted'] = 'Quiz attempt submitted';
|
||||
$string['eventquizattempttimelimitexceeded'] = 'Quiz attempt time limit exceeded';
|
||||
$string['eventquizgradeupdated'] = 'Quiz grade updated';
|
||||
$string['eventquizrepaginated'] = 'Quiz re-paginated';
|
||||
$string['eventreportviewed'] = 'Quiz report viewed';
|
||||
@ -791,6 +792,7 @@ $string['quizordernotrandom'] = 'Order of quiz not shuffled';
|
||||
$string['quizorderrandom'] = '* Order of quiz is shuffled';
|
||||
$string['quiz:preview'] = 'Preview quizzes';
|
||||
$string['quiz:regrade'] = 'Regrade quiz attempts';
|
||||
$string['quiz:reopenattempts'] = 'Re-open never submitted quiz attempts';
|
||||
$string['quizreport'] = 'Quiz report';
|
||||
$string['quiz:reviewmyattempts'] = 'Review your own attempts';
|
||||
$string['quizsettings'] = 'Quiz settings';
|
||||
@ -836,6 +838,13 @@ $string['removepagebreak'] = 'Remove page break';
|
||||
$string['removeselected'] = 'Remove selected';
|
||||
$string['rename'] = 'Rename';
|
||||
$string['renderingserverconnectfailed'] = 'The server {$a} failed to process an RQP request. Check that the URL is correct.';
|
||||
$string['reopenattempt'] = 'Re-open';
|
||||
$string['reopenattemptareyousuremessage'] = 'Are you sure you wish to re-open quiz attempt {$a->attemptnumber} by {$a->attemptuser}?';
|
||||
$string['reopenattemptareyousuretitle'] = 'Confirm re-open';
|
||||
$string['reopenattemptwrongstate'] = 'Attempt {$a->attemptid} is in the wrong state ({$a->state}) to be re-openend.';
|
||||
$string['reopenedattemptwillbeinprogress'] = 'The re-opened attempt will remain open so that it can be continued.';
|
||||
$string['reopenedattemptwillbeinprogressuntil'] = 'The re-opened attempt will remain open so that it can be continued. It will be due for submission by {$a}.';
|
||||
$string['reopenedattemptwillbesubmitted'] = 'The re-opened attempt will be immediately submitted for grading.';
|
||||
$string['reorderquestions'] = 'Reorder questions';
|
||||
$string['reordertool'] = 'Show the reordering tool';
|
||||
$string['repaginate'] = 'Repaginate with {$a} questions per page';
|
||||
|
49
mod/quiz/report/overview/tests/behat/reopen_attempt.feature
Normal file
49
mod/quiz/report/overview/tests/behat/reopen_attempt.feature
Normal file
@ -0,0 +1,49 @@
|
||||
@mod @mod_quiz @quiz @quiz_overview @javascript
|
||||
Feature: Re-opening Never submitted quiz attempts
|
||||
In order to cut some slack to students who forgot to submit their quiz attempt
|
||||
As a teacher
|
||||
I need to be able to re-open selected attempts.
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
| teacher | Mark | Allwright |
|
||||
| student | Freddy | Forgetful |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
| student | C1 | student |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name |
|
||||
| Test questions | truefalse | TF |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And quiz "Test quiz" contains the following questions:
|
||||
| question | page |
|
||||
| TF | 1 |
|
||||
And user "student" has started an attempt at quiz "Test quiz"
|
||||
And the attempt at "Test quiz" by "student" was never submitted
|
||||
|
||||
Scenario: Attempt can be re-opened
|
||||
Given I am on the "Test quiz" "mod_quiz > Grades report" page logged in as teacher
|
||||
When I press "Re-open"
|
||||
And I should see "Are you sure you wish to re-open quiz attempt 1 by Freddy Forgetful?" in the "Confirm re-open" "dialogue"
|
||||
And I should see "The re-opened attempt will remain open so that it can be continued." in the "Confirm re-open" "dialogue"
|
||||
And I click on "Re-open" "button" in the "Confirm re-open" "dialogue"
|
||||
Then I should see "In progress" in the "Freddy Forgetful" "table_row"
|
||||
And "Re-open" "button" should not exist
|
||||
|
||||
Scenario: Re-opening an attempt can be cancelled and then nothing happens
|
||||
Given I am on the "Test quiz" "mod_quiz > Grades report" page logged in as teacher
|
||||
And I start watching to see if a new page loads
|
||||
When I press "Re-open"
|
||||
And I click on "Cancel" "button" in the "Confirm re-open" "dialogue"
|
||||
Then a new page should not have loaded since I started watching
|
||||
And I should see "Never submitted" in the "Freddy Forgetful" "table_row"
|
@ -16,9 +16,9 @@
|
||||
|
||||
namespace mod_quiz;
|
||||
|
||||
use moodle_url;
|
||||
use question_bank;
|
||||
use question_engine;
|
||||
use mod_quiz\quiz_settings;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@ -28,12 +28,12 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
/**
|
||||
* Quiz attempt walk through.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category test
|
||||
* @copyright 2013 The Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \quiz_attempt
|
||||
* @package mod_quiz
|
||||
* @category test
|
||||
* @copyright 2013 The Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz\quiz_attempt
|
||||
*/
|
||||
class attempt_walkthrough_test extends \advanced_testcase {
|
||||
|
||||
@ -154,9 +154,10 @@ class attempt_walkthrough_test extends \advanced_testcase {
|
||||
* The quiz is set to close 1 hour from now.
|
||||
* The quiz is set to use a grade period of 1 hour once time expires.
|
||||
*
|
||||
* @param string $overduehandling value for the overduehandling quiz setting.
|
||||
* @return \stdClass the quiz that was created.
|
||||
*/
|
||||
protected function create_quiz_with_one_question(): \stdClass {
|
||||
protected function create_quiz_with_one_question(string $overduehandling = 'graceperiod'): \stdClass {
|
||||
global $SITE;
|
||||
$this->resetAfterTest();
|
||||
|
||||
@ -166,7 +167,7 @@ class attempt_walkthrough_test extends \advanced_testcase {
|
||||
|
||||
$quiz = $quizgenerator->create_instance(
|
||||
['course' => $SITE->id, 'timeclose' => $timeclose,
|
||||
'overduehandling' => 'graceperiod', 'graceperiod' => HOURSECS]);
|
||||
'overduehandling' => $overduehandling, 'graceperiod' => HOURSECS]);
|
||||
|
||||
// Create a question.
|
||||
/** @var \core_question_generator $questiongenerator */
|
||||
@ -461,4 +462,120 @@ class attempt_walkthrough_test extends \advanced_testcase {
|
||||
$gradebookgrade = array_shift($gradebookitem->grades);
|
||||
$this->assertEquals(100, $gradebookgrade->grade);
|
||||
}
|
||||
|
||||
public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_with_timelimit_override() {
|
||||
global $DB;
|
||||
|
||||
$quiz = $this->create_quiz_with_one_question('autoabandon');
|
||||
$originaltimeclose = $quiz->timeclose;
|
||||
|
||||
// Make a user to do the quiz.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
$quizobj = quiz_settings::create($quiz->id, $user->id);
|
||||
|
||||
// Start the attempt.
|
||||
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
|
||||
|
||||
// Process some responses from the student during the attempt.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
|
||||
|
||||
// Student leaves, so cron closes the attempt when time expires.
|
||||
$attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
|
||||
|
||||
// Verify the attempt state.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
|
||||
$this->assertEquals(0, $attemptobj->get_submitted_date());
|
||||
$this->assertEquals($user->id, $attemptobj->get_userid());
|
||||
|
||||
// The teacher feels kind, so adds an override for the student, and re-opens the attempt.
|
||||
$sink = $this->redirectEvents();
|
||||
$overriddentimeclose = $originaltimeclose + HOURSECS;
|
||||
$DB->insert_record('quiz_overrides', [
|
||||
'quiz' => $quiz->id,
|
||||
'userid' => $user->id,
|
||||
'timeclose' => $overriddentimeclose,
|
||||
]);
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$reopentime = $originaltimeclose + 10 * MINSECS;
|
||||
$attemptobj->process_reopen_abandoned($reopentime);
|
||||
|
||||
// Verify the attempt state.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertEquals(1, $attemptobj->get_attempt_number());
|
||||
$this->assertFalse($attemptobj->is_finished());
|
||||
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
|
||||
$this->assertEquals(0, $attemptobj->get_submitted_date());
|
||||
$this->assertEquals($user->id, $attemptobj->get_userid());
|
||||
$this->assertEquals($overriddentimeclose,
|
||||
$attemptobj->get_access_manager($reopentime)->get_end_time($attemptobj->get_attempt()));
|
||||
|
||||
// Verify this was logged correctly.
|
||||
$events = $sink->get_events();
|
||||
$this->assertCount(1, $events);
|
||||
|
||||
$reopenedevent = array_shift($events);
|
||||
$this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
|
||||
$this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
|
||||
$this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
|
||||
$reopenedevent->get_url());
|
||||
}
|
||||
|
||||
public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_after_close_time() {
|
||||
$quiz = $this->create_quiz_with_one_question('autoabandon');
|
||||
$originaltimeclose = $quiz->timeclose;
|
||||
|
||||
// Make a user to do the quiz.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
$quizobj = quiz_settings::create($quiz->id, $user->id);
|
||||
|
||||
// Start the attempt.
|
||||
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
|
||||
|
||||
// Process some responses from the student during the attempt.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
|
||||
|
||||
// Student leaves, so cron closes the attempt when time expires.
|
||||
$attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
|
||||
|
||||
// Verify the attempt state.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
|
||||
$this->assertEquals(0, $attemptobj->get_submitted_date());
|
||||
$this->assertEquals($user->id, $attemptobj->get_userid());
|
||||
|
||||
// The teacher reopens the attempt without granting more time, so previously submitted responess are graded.
|
||||
$sink = $this->redirectEvents();
|
||||
$reopentime = $originaltimeclose + 10 * MINSECS;
|
||||
$attemptobj->process_reopen_abandoned($reopentime);
|
||||
|
||||
// Verify the attempt state.
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$this->assertEquals(1, $attemptobj->get_attempt_number());
|
||||
$this->assertTrue($attemptobj->is_finished());
|
||||
$this->assertEquals(quiz_attempt::FINISHED, $attemptobj->get_state());
|
||||
$this->assertEquals($originaltimeclose, $attemptobj->get_submitted_date());
|
||||
$this->assertEquals($user->id, $attemptobj->get_userid());
|
||||
$this->assertEquals(1, $attemptobj->get_sum_marks());
|
||||
|
||||
// Verify this was logged correctly - there are some gradebook events between the two we want to check.
|
||||
$events = $sink->get_events();
|
||||
$this->assertGreaterThanOrEqual(2, $events);
|
||||
|
||||
$reopenedevent = array_shift($events);
|
||||
$this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
|
||||
$this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
|
||||
$this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
|
||||
$reopenedevent->get_url());
|
||||
|
||||
$submittedevent = array_pop($events);
|
||||
$this->assertInstanceOf('\mod_quiz\event\attempt_submitted', $submittedevent);
|
||||
$this->assertEquals($attemptobj->get_context(), $submittedevent->get_context());
|
||||
$this->assertEquals(new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
|
||||
$submittedevent->get_url());
|
||||
}
|
||||
}
|
||||
|
@ -842,10 +842,6 @@ class behat_mod_quiz extends behat_question_base {
|
||||
/**
|
||||
* Start a quiz attempt without answers.
|
||||
*
|
||||
* Then there should be a number of rows of data, one for each question you want to add.
|
||||
* There is no need to supply answers to all questions. If so, other qusetions will be
|
||||
* left unanswered.
|
||||
*
|
||||
* @param string $username the username of the user that will attempt.
|
||||
* @param string $quizname the name of the quiz the user will attempt.
|
||||
* @Given /^user "([^"]*)" has started an attempt at quiz "([^"]*)"$/
|
||||
@ -988,6 +984,31 @@ class behat_mod_quiz extends behat_question_base {
|
||||
$this->set_user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish an existing quiz attempt.
|
||||
*
|
||||
* @param string $quizname the name of the quiz the user will attempt.
|
||||
* @param string $username the username of the user that will attempt.
|
||||
* @Given the attempt at :quizname by :username was never submitted
|
||||
*/
|
||||
public function attempt_was_abandoned($quizname, $username) {
|
||||
global $DB;
|
||||
|
||||
$quizid = $DB->get_field('quiz', 'id', ['name' => $quizname], MUST_EXIST);
|
||||
$user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
|
||||
|
||||
$this->set_user($user);
|
||||
|
||||
$attempt = quiz_get_user_attempt_unfinished($quizid, $user->id);
|
||||
if (!$attempt) {
|
||||
throw new coding_exception("No in-progress attempt found for $username and quiz $quizname.");
|
||||
}
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
$attemptobj->process_abandon(time(), false);
|
||||
|
||||
$this->set_user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the exact named selectors for the component.
|
||||
*
|
||||
|
177
mod/quiz/tests/external/reopen_attempt_test.php
vendored
Normal file
177
mod/quiz/tests/external/reopen_attempt_test.php
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
<?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\external;
|
||||
|
||||
use coding_exception;
|
||||
use core_question_generator;
|
||||
use externallib_advanced_testcase;
|
||||
use mod_quiz\quiz_attempt;
|
||||
use mod_quiz\quiz_settings;
|
||||
use required_capability_exception;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Test for the reopen_attempt and get_reopen_attempt_confirmation services.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category external
|
||||
* @copyright 2023 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz\external\reopen_attempt
|
||||
* @covers \mod_quiz\external\get_reopen_attempt_confirmation
|
||||
*/
|
||||
class reopen_attempt_test extends externallib_advanced_testcase {
|
||||
/** @var stdClass|null if we make a quiz attempt, we store the student object here. */
|
||||
protected $student;
|
||||
|
||||
public function test_reopen_attempt_service_works() {
|
||||
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
|
||||
|
||||
reopen_attempt::execute($attemptid);
|
||||
|
||||
$attemptobj = quiz_attempt::create($attemptid);
|
||||
$this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
|
||||
}
|
||||
|
||||
public function test_reopen_attempt_service_checks_permissions() {
|
||||
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
|
||||
|
||||
$unprivilegeduser = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($unprivilegeduser);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
reopen_attempt::execute($attemptid);
|
||||
}
|
||||
|
||||
public function test_reopen_attempt_service_checks_attempt_state() {
|
||||
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question(quiz_attempt::IN_PROGRESS);
|
||||
|
||||
$this->expectExceptionMessage("Attempt $attemptid is in the wrong state (In progress) to be re-openend.");
|
||||
reopen_attempt::execute($attemptid);
|
||||
}
|
||||
|
||||
public function test_get_reopen_attempt_confirmation_staying_open() {
|
||||
global $DB;
|
||||
[$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
|
||||
$DB->set_field('quiz', 'timeclose', 0, ['id' => $quizid]);
|
||||
|
||||
$message = get_reopen_attempt_confirmation::execute($attemptid);
|
||||
|
||||
$this->assertEquals('<p>Are you sure you wish to re-open quiz attempt 1 by ' . fullname($this->student) .
|
||||
'?</p><p>The re-opened attempt will remain open so that it can be continued.</p>',
|
||||
$message);
|
||||
}
|
||||
|
||||
public function test_get_reopen_attempt_confirmation_staying_open_until() {
|
||||
global $DB;
|
||||
[$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
|
||||
$timeclose = time() + HOURSECS;
|
||||
$DB->set_field('quiz', 'timeclose', $timeclose, ['id' => $quizid]);
|
||||
|
||||
$message = get_reopen_attempt_confirmation::execute($attemptid);
|
||||
|
||||
$this->assertEquals('<p>Are you sure you wish to re-open quiz attempt 1 by ' . fullname($this->student) .
|
||||
'?</p><p>The re-opened attempt will remain open so that it can be continued. It will be due for submission by ' .
|
||||
userdate($timeclose) . '.</p>',
|
||||
$message);
|
||||
}
|
||||
|
||||
public function test_get_reopen_attempt_confirmation_submitting() {
|
||||
global $DB;
|
||||
[$attemptid, $quizid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
|
||||
$timeclose = time() - HOURSECS;
|
||||
$DB->set_field('quiz', 'timeclose', $timeclose, ['id' => $quizid]);
|
||||
|
||||
$message = get_reopen_attempt_confirmation::execute($attemptid);
|
||||
|
||||
$this->assertEquals('<p>Are you sure you wish to re-open quiz attempt 1 by ' . fullname($this->student) .
|
||||
'?</p><p>The re-opened attempt will be immediately submitted for grading.</p>',
|
||||
$message);
|
||||
}
|
||||
|
||||
public function test_get_reopen_attempt_confirmation_service_checks_permissions() {
|
||||
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question();
|
||||
|
||||
$unprivilegeduser = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($unprivilegeduser);
|
||||
|
||||
$this->expectException(required_capability_exception::class);
|
||||
get_reopen_attempt_confirmation::execute($attemptid);
|
||||
}
|
||||
|
||||
public function test_get_reopen_attempt_confirmation_service_checks_attempt_state() {
|
||||
[$attemptid] = $this->create_attempt_at_quiz_with_one_shortanswer_question(quiz_attempt::IN_PROGRESS);
|
||||
|
||||
$this->expectExceptionMessage("Attempt $attemptid is in the wrong state (In progress) to be re-openend.");
|
||||
get_reopen_attempt_confirmation::execute($attemptid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a quiz of one shortanswer question and an attempt in a given state.
|
||||
*
|
||||
* @param string $attemptstate the desired attempt state. quiz_attempt::ABANDONED or ::IN_PROGRESS.
|
||||
* @return array with two elements, the attempt id and the quiz id.
|
||||
*/
|
||||
protected function create_attempt_at_quiz_with_one_shortanswer_question(
|
||||
string $attemptstate = quiz_attempt::ABANDONED
|
||||
): array {
|
||||
global $SITE;
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Make a quiz.
|
||||
$timeclose = time() + HOURSECS;
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
|
||||
$quiz = $quizgenerator->create_instance([
|
||||
'course' => $SITE->id,
|
||||
'timeclose' => $timeclose,
|
||||
'overduehandling' => 'autoabandon'
|
||||
]);
|
||||
|
||||
// Create a question.
|
||||
/** @var core_question_generator $questiongenerator */
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
$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);
|
||||
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
|
||||
|
||||
// Make a user to do the quiz.
|
||||
$this->student = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($this->student);
|
||||
$quizobj = quiz_settings::create($quiz->id, $this->student->id);
|
||||
|
||||
// Start the attempt.
|
||||
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
|
||||
$attemptobj = quiz_attempt::create($attempt->id);
|
||||
|
||||
if ($attemptstate === quiz_attempt::ABANDONED) {
|
||||
// Attempt goes overdue (e.g. if cron ran).
|
||||
$attemptobj->process_abandon($timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
|
||||
} else if ($attemptstate !== quiz_attempt::IN_PROGRESS) {
|
||||
throw new coding_exception('State ' . $attemptstate . ' not currently supported.');
|
||||
}
|
||||
|
||||
// Set current user to admin before we return.
|
||||
$this->setAdminUser();
|
||||
|
||||
return [$attemptobj->get_attemptid(), $attemptobj->get_quizid()];
|
||||
}
|
||||
}
|
@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2022120500;
|
||||
$plugin->requires = 2022111800;
|
||||
$plugin->version = 2023030300;
|
||||
$plugin->requires = 2022111800;
|
||||
$plugin->component = 'mod_quiz';
|
||||
|
Loading…
x
Reference in New Issue
Block a user