MDL-73699 qbank_editquestion: Question status UI/UX update

This commit implements a question status UI/UX change in the question bank page
from clickable popup link to dropdown following moodle UI/UX policy.
This commit is contained in:
Marc-Alexandre Ghaly
2022-02-17 08:46:30 -05:00
parent 6652ec135b
commit a21764842c
12 changed files with 154 additions and 227 deletions
@@ -1,2 +1,2 @@
function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("qbank_editquestion/question_status",["exports","core/fragment","core/str","core/modal_factory","core/notification","core/modal_events","core/ajax"],function(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=j(b);c=i(c);d=j(d);e=j(e);f=j(f);g=j(g);function h(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;h=function(){return a};return a}function i(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=h();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function j(a){return a&&a.__esModule?a:{default:a}}var k=function(a,c){return b.default.loadFragment("qbank_editquestion","question_status",c,a)},l=function(a,b){return g.default.call([{methodname:"qbank_editquestion_set_status",args:{questionid:a,formdata:b}}])[0]},m=function(a,b,c){var d=a.getBody().find("form").serialize();l(b,d).then(function(a){if(a.status){c.innerText=a.statusname}}).catch(e.default.exception)},n=function(a,b,c){o({questionid:a},b).then(function(b){b.show();var d=b.getRoot();d.on(f.default.save,function(d){d.preventDefault();d.stopPropagation();m(b,a,c);b.hide()});return b}).catch(e.default.exception)},o=function(a,b){return d.default.create({type:d.default.types.SAVE_CANCEL,title:c.get_string("questionstatusheader","qbank_editquestion"),body:k(a,b),large:!1})};a.init=function init(a,b){var c=document.querySelector(a),d=c.getAttribute("data-questionid");c.addEventListener("click",function(){n(d,b,c)})}});
define ("qbank_editquestion/question_status",["exports","core/ajax","core/notification"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f=function(a,c){return b.default.call([{methodname:"qbank_editquestion_set_status",args:{questionid:a,status:c}}])[0]};a.init=function init(a){var b=document.querySelector("#question_status_dropdown-"+a);b.addEventListener("change",function(b){var d=b.target.value;f(a,d).then(function(a){if(a.error){c.default.addNotification({type:"error",message:a.error})}}).catch()})}});
//# sourceMappingURL=question_status.min.js.map
File diff suppressed because one or more lines are too long
@@ -22,114 +22,43 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Fragment from 'core/fragment';
import * as Str from 'core/str';
import ModalFactory from 'core/modal_factory';
import Notification from 'core/notification';
import ModalEvents from 'core/modal_events';
import Ajax from 'core/ajax';
/**
* Get the fragment.
*
* @method getFragment
* @param {{questioned: Number}} args
* @param {Number} contextId
* @return {String}
*/
const getFragment = (args, contextId) => {
return Fragment.loadFragment('qbank_editquestion', 'question_status', contextId, args);
};
import Notification from 'core/notification';
/**
* Set the question status.
*
* @param {Number} questionId The question id.
* @param {String} formData The question tag form data in a URI encoded param string
* @param {String} status The updated question status.
* @return {Array} The modified question status
*/
const setQuestionStatus = (questionId, formData) => Ajax.call([{
const setQuestionStatus = (questionId, status) => Ajax.call([{
methodname: 'qbank_editquestion_set_status',
args: {
questionid: questionId,
formdata: formData
status: status
}
}])[0];
/**
* Save the status.
*
* @method getFragment
* @param {object} modal
* @param {Number} questionId
* @param {HTMLElement} target
*/
const save = (modal, questionId, target) => {
const formData = modal.getBody().find('form').serialize();
setQuestionStatus(questionId, formData)
.then(result => {
if (result.status) {
target.innerText = result.statusname;
}
return;
})
.catch(Notification.exception);
};
/**
* Event listeners for the module.
*
* @method clickEvent
* @param {Number} questionId
* @param {Number} contextId
* @param {HTMLElement} target
*/
const statusEvent = (questionId, contextId, target) => {
let args = {
questionid: questionId
};
getStatusModal(args, contextId)
.then((modal) => {
modal.show();
let root = modal.getRoot();
root.on(ModalEvents.save, function(e) {
e.preventDefault();
e.stopPropagation();
save(modal, questionId, target);
modal.hide();
});
return modal;
})
.catch(Notification.exception);
};
/**
* Get the status modal to display.
*
* @param {{questionid: Number}} args
* @param {Number} contextId
* @return {HTMLElement}
*/
const getStatusModal = (args, contextId) => ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
title: Str.get_string('questionstatusheader', 'qbank_editquestion'),
body: getFragment(args, contextId),
large: false,
});
/**
* Entrypoint of the js.
*
* @method init
* @param {String} questionSelector the question status identifier.
* @param {Number} contextId The context id of the question.
* @param {Number} questionId Question id.
*/
export const init = (questionSelector, contextId) => {
let target = document.querySelector(questionSelector);
let questionId = target.getAttribute('data-questionid');
target.addEventListener('click', () => {
// Call for the event listener to listed for clicks in any usage count row.
statusEvent(questionId, contextId, target);
export const init = (questionId) => {
let target = document.querySelector('#question_status_dropdown-' + questionId);
target.addEventListener('change', (e) => {
const questionStatus = e.target.value;
setQuestionStatus(questionId, questionStatus)
.then((response) => {
if (response.error) {
Notification.addNotification({
type: 'error',
message: response.error
});
}
return;
}).catch();
});
};
@@ -46,7 +46,7 @@ class update_question_version_status extends \external_api {
public static function execute_parameters() {
return new external_function_parameters([
'questionid' => new external_value(PARAM_INT, 'The question id'),
'formdata' => new external_value(PARAM_RAW, 'The data from the status form'),
'status' => new external_value(PARAM_TEXT, 'The updated question status')
]);
}
@@ -54,44 +54,49 @@ class update_question_version_status extends \external_api {
* Handles the status form submission.
*
* @param int $questionid The question id.
* @param string $formdata The question tag form data in a URI encoded param string
* @param string $status The updated question status.
* @return array The created or modified question tag
*/
public static function execute($questionid, $formdata) {
public static function execute($questionid, $status) {
global $DB;
$data = [];
$result = [
'status' => false,
'statusname' => ''
'statusname' => '',
'error' => ''
];
// Parameter validation.
$params = self::validate_parameters(self::execute_parameters(), [
'questionid' => $questionid,
'formdata' => $formdata
'status' => $status
]);
parse_str($params['formdata'], $data);
$statuslist = editquestion_helper::get_question_status_list();
$statusexists = array_key_exists($status, $statuslist);
if (!$statusexists) {
return [
'status' => false,
'statusname' => '',
'error' => get_string('unrecognizedstatus', 'qbank_editquestion')
];
}
$question = question_bank::load_question($params['questionid']);
$editingcontext = \context::instance_by_id($question->contextid);
self::validate_context($editingcontext);
$canedit = question_has_capability_on($question, 'edit');
$mform = new \qbank_editquestion\form\question_status_form(null, null, 'post', '', null, $canedit, $data);
if ($validateddata = $mform->get_data()) {
if ($canedit && isset($validateddata->status)) {
$versionrecord = $DB->get_record('question_versions', ['questionid' => $params['questionid']]);
$versionrecord->status = $validateddata->status;
$DB->update_record('question_versions', $versionrecord);
question_bank::notify_question_edited($question->id);
$result = [
'status' => true,
'statusname' => editquestion_helper::get_question_status_string($versionrecord->status)
];
$event = \core\event\question_updated::create_from_question_instance($question, $editingcontext);
$event->trigger();
}
if ($canedit) {
$versionrecord = $DB->get_record('question_versions', ['questionid' => $params['questionid']]);
$versionrecord->status = $params['status'];
$DB->update_record('question_versions', $versionrecord);
question_bank::notify_question_edited($question->id);
$result = [
'status' => true,
'statusname' => editquestion_helper::get_question_status_string($versionrecord->status),
'error' => ''
];
$event = \core\event\question_updated::create_from_question_instance($question, $editingcontext);
$event->trigger();
}
return $result;
@@ -103,7 +108,8 @@ class update_question_version_status extends \external_api {
public static function execute_returns() {
return new external_single_structure([
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'statusname' => new external_value(PARAM_RAW, 'statusname: name of the status')
'statusname' => new external_value(PARAM_RAW, 'statusname: name of the status'),
'error' => new external_value(PARAM_TEXT, 'Error message if error exists')
]);
}
}
@@ -1,39 +0,0 @@
<?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 qbank_editquestion\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/formslib.php');
/**
* Class question_status_form to change the question status using a modal.
*
* @package qbank_editquestion
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_status_form extends \moodleform {
public function definition() {
$mform = $this->_form;
$mform->disable_form_change_checker();
$mform->addElement('select', 'status', get_string('status', 'qbank_editquestion'),
\qbank_editquestion\editquestion_helper::get_question_status_list());
}
}
@@ -67,4 +67,13 @@ class renderer extends \plugin_renderer_base {
return $this->render_from_template('qbank_editquestion/question_info', $questiondata);
}
/**
* Render status dropdown.
*
* @param array $dropdownoptions
* @return bool|string
*/
public function render_status_dropdown($dropdownoptions) {
return $this->render_from_template('qbank_editquestion/question_status_dropdown', $dropdownoptions);
}
}
@@ -42,18 +42,19 @@ class question_status_column extends column_base {
$attributes = [];
if (question_has_capability_on($question, 'edit')
&& $question->status !== question_version_status::QUESTION_STATUS_HIDDEN) {
$target = 'questionstatus_' . $question->id;
$datatarget = '[data-target="' . $target . '"]';
$PAGE->requires->js_call_amd('qbank_editquestion/question_status', 'init', [$datatarget, $question->contextid]);
$attributes = [
'data-target' => $target,
'data-questionid' => $question->id,
'data-courseid' => $this->qbank->course->id,
'class' => 'link-primary comment-pointer',
'href' => '#'
];
$options = [];
$options['questionid'] = $question->id;
$statuslist = editquestion_helper::get_question_status_list();
foreach ($statuslist as $value => $displaystatus) {
$options['options'][] = [
'name' => $displaystatus,
'value' => $value,
'selected' => ($question->status) === $value ? true : false
];
}
echo $PAGE->get_renderer('qbank_editquestion')->render_status_dropdown($options);
$PAGE->requires->js_call_amd('qbank_editquestion/question_status', 'init', [$question->id]);
}
echo \html_writer::tag('a', editquestion_helper::get_question_status_string($question->status), $attributes);
}
}
@@ -32,6 +32,7 @@ $string['questionstatusready'] = 'Ready';
$string['questionstatushidden'] = 'Hidden';
$string['questionstatusdraft'] = 'Draft';
$string['questionstatusheader'] = 'Change question status';
$string['unrecognizedstatus'] = 'Unrecognized status';
// Edit form.
$string['versioninfo'] = 'Version';
-41
View File
@@ -1,41 +0,0 @@
<?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/>.
/**
* Helper functions and callbacks.
*
* @package qbank_editquestion
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Question status fragment callback.
*
* @param array $args
* @return string rendered output
*/
function qbank_editquestion_output_fragment_question_status($args): string {
global $CFG;
require_once($CFG->dirroot . '/question/engine/bank.php');
$question = question_bank::load_question($args['questionid']);
$mform = new \qbank_editquestion\form\question_status_form();
$data = ['status' => $question->status];
$mform->set_data($data);
return $mform->render();
}
@@ -0,0 +1,36 @@
{{!
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/>.
}}
{{!
@template qbank_editquestion/question_status_dropdown
Dropdrown selector for question status in question bank.
Context variables required for this template:
* options - list of options for question status containing name, value, selected.
* questionid - Question id for status selection.
Example context (json):
{
"options" : [ { "name": "Ready", "value": "ready", "selected": true } ],
"questionid" : "12"
}
}}
<select id="question_status_dropdown-{{questionid}}" class="custom-select my-2" name="question_status_dropdown">
{{#options}}
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
{{/options}}
</select>
@@ -13,19 +13,29 @@ Feature: Use the qbank base view to test the status change using
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
| Test questions | truefalse | Second question | Answer the first question |
@javascript
Scenario: Question status modal should change the status of the question
Scenario: Question status dropdown should change the status of the question
Given I log in as "admin"
And I am on the "Test quiz" "quiz activity" page
And I navigate to "Question bank > Questions" in current page administration
And I set the field "Select a category" to "Test questions"
And I should see "Test questions"
And I should see "Ready" in the "First question" "table_row"
When I click on "Ready" "link" in the "First question" "table_row"
Then I should see "Change question status"
And I should see "Question status"
And I click on "Close" "button" in the ".modal-dialog" "css_element"
And I should see "Ready" in the "First question" "table_row"
And I should see "Ready" in the "Second question" "table_row"
And I click on "question_status_dropdown" "select" in the "First question" "table_row"
And I should see "Draft"
And I click on "Draft" "option"
And I reload the page
And I should see "Draft" in the "First question" "table_row"
And I should see "Ready" in the "Second question" "table_row"
And I click on "question_status_dropdown" "select" in the "Second question" "table_row"
And I click on "Draft" "option"
And I click on "question_status_dropdown" "select" in the "First question" "table_row"
And I click on "Ready" "option"
And I reload the page
Then I should see "Ready" in the "First question" "table_row"
And I should see "Draft" in the "Second question" "table_row"
@@ -16,6 +16,7 @@
namespace qbank_editquestion;
use core_question\local\bank\question_version_status;
use qbank_editquestion\external\update_question_version_status;
/**
@@ -26,7 +27,7 @@ use qbank_editquestion\external\update_question_version_status;
* @author Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_question\local\bank\question_version_status
* @coversDefaultClass \qbank_editquestion\form\question_status_form
* @coversDefaultClass \qbank_editquestion\external\update_question_version_status
* @coversDefaultClass \qbank_editquestion\editquestion_helper
*/
class update_question_version_status_test extends \advanced_testcase {
@@ -45,7 +46,6 @@ class update_question_version_status_test extends \advanced_testcase {
/**
* Test if the submit status webservice changes the status of the question.
*
* @covers ::mock_generate_submit_keys
* @covers ::execute
* @covers ::get_question_status_string
*/
@@ -56,20 +56,38 @@ class update_question_version_status_test extends \advanced_testcase {
$cat = $questiongenerator->create_question_category();
$numq = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is the first version']);
$data = ['status' => 2];
$mform = \qbank_editquestion\form\question_status_form::mock_generate_submit_keys($data);
$this->expectException('moodle_exception');
list($result, $statusname) = update_question_version_status::execute($numq->id, http_build_query($mform, '', '&'));
$result = update_question_version_status::execute($numq->id, 'draft');
// Test if the version actually changed.
$currentstatus = $DB->get_record('question_versions', ['questionid' => $numq->id]);
$this->assertEquals($data['status'], $currentstatus->status);
$this->assertEquals(editquestion_helper::get_question_status_string($currentstatus->status), $statusname);
$this->assertEquals(editquestion_helper::get_question_status_string($currentstatus->status), $result['statusname']);
}
/**
* Test submit status webservice only takes an existing parameter status.
*
* @covers ::execute
*/
public function test_submit_status_error() {
global $DB;
$this->resetAfterTest();
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$numq = $questiongenerator->create_question('essay', null,
['category' => $cat->id, 'name' => 'This is the first version']);
// Passing a wrong status to web service.
$result = update_question_version_status::execute($numq->id, 'frog');
// Tests web service returns error.
$this->assertEquals(false, $result['status']);
$this->assertEquals('', $result['statusname']);
$this->assertEquals(get_string('unrecognizedstatus', 'qbank_editquestion'), $result['error']);
// Test version did not change.
$currentstatus = $DB->get_record('question_versions', ['questionid' => $numq->id]);
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $currentstatus->status);
}
/**
* Test that updating the status does not create a new version.
*
* @covers ::mock_generate_submit_keys
* @covers ::execute
*/
public function test_submit_status_does_not_create_a_new_version() {
@@ -81,10 +99,7 @@ class update_question_version_status_test extends \advanced_testcase {
['category' => $cat->id, 'name' => 'This is the first version']);
$countcurrentrecords = $DB->count_records('question_versions');
$this->assertEquals(1, $countcurrentrecords);
$data = ['status' => 2];
$mform = \qbank_editquestion\form\question_status_form::mock_generate_submit_keys($data);
$this->expectException('moodle_exception');
list($result, $statusname) = update_question_version_status::execute($numq->id, http_build_query($mform, '', '&'));
$result = update_question_version_status::execute($numq->id, 'draft');
$countafterupdate = $DB->count_records('question_versions');
$this->assertEquals($countcurrentrecords, $countafterupdate);
}