mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 14:03:52 +01:00
MDL-35745 quiz: let teachers to re-open a Never submitted attempt
In the quiz reports, for any 'Never submitted' attempt, there is now a 'Re-open' button next to where it says the attempt state. If that is clicked, there is an 'Are you sure?' pop-up. If the user continues, the attempt is reopened. If the student now has time left, the attempt is put into the In progress state. If there is not time left the attempt is immediately submitted and graded. The 'Are you sure? pop-up says which of those two things will happen.
This commit is contained in:
parent
5e1df25566
commit
c051fbd0c2
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