mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 08:26:37 +02:00
Merge branch 'master_MDL-71696-versioning-integration' of https://github.com/catalyst/moodle-MDL-70329
This commit is contained in:
commit
b841a811be
223 changed files with 7768 additions and 2899 deletions
|
@ -38,7 +38,9 @@ class helper {
|
|||
list($usql, $params) = $DB->get_in_or_equal($questionids);
|
||||
$sql = "SELECT q.*, c.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} c ON c.id = q.category
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} c ON c.id = qbe.questioncategoryid
|
||||
WHERE q.id
|
||||
{$usql}";
|
||||
$questions = $DB->get_records_sql($sql, $params);
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once(__DIR__ . '/../../editlib.php');
|
||||
|
||||
global $DB, $OUTPUT, $PAGE, $COURSE;
|
||||
|
||||
$moveselected = optional_param('move', false, PARAM_BOOL);
|
||||
|
@ -52,7 +53,7 @@ if ($cmid) {
|
|||
throw new moodle_exception('missingcourseorcmid', 'question');
|
||||
}
|
||||
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$url = new moodle_url('/question/bank/bulkmove/move.php');
|
||||
|
||||
$PAGE->set_url($url);
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
namespace qbank_bulkmove;
|
||||
|
||||
use core_question\local\bank\question_edit_contexts;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
|
@ -53,7 +55,7 @@ class helper_test extends \advanced_testcase {
|
|||
protected $context;
|
||||
|
||||
/**
|
||||
* @var \question_edit_contexts $contexts
|
||||
* @var \core_question\local\bank\question_edit_contexts $contexts
|
||||
*/
|
||||
protected $contexts;
|
||||
|
||||
|
@ -87,7 +89,7 @@ class helper_test extends \advanced_testcase {
|
|||
$this->context = \context_course::instance($this->course->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$this->contexts = new \question_edit_contexts($this->context);
|
||||
$this->contexts = new question_edit_contexts($this->context);
|
||||
$this->cat = question_make_default_categories($this->contexts->all());
|
||||
$this->questiondata1 = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $this->cat->id]);
|
||||
|
@ -202,7 +204,7 @@ class helper_test extends \advanced_testcase {
|
|||
public function test_get_displaydata() {
|
||||
$this->helper_setup();
|
||||
$coursecontext = \context_course::instance($this->course->id);
|
||||
$contexts = new \question_edit_contexts($coursecontext);
|
||||
$contexts = new question_edit_contexts($coursecontext);
|
||||
$addcontexts = $contexts->having_cap('moodle/question:add');
|
||||
$url = new \moodle_url('/question/bank/bulkmove/move.php');
|
||||
$displaydata = \qbank_bulkmove\helper::get_displaydata($addcontexts, $url, $url);
|
||||
|
|
|
@ -137,7 +137,8 @@ class qbank_comment_backup_restore_test extends \advanced_testcase {
|
|||
* across the backup and restore process.
|
||||
*/
|
||||
public function test_backup_restore() {
|
||||
global $DB;
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->dirroot . '/comment/lib.php');
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
|
@ -195,12 +196,18 @@ class qbank_comment_backup_restore_test extends \advanced_testcase {
|
|||
$this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id);
|
||||
|
||||
// The questions and their associated comments should have been restored.
|
||||
$newquestion1 = $DB->get_record('question', ['idnumber' => 'q1']);
|
||||
$sql =
|
||||
'SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.idnumber = ?';
|
||||
$newquestion1 = $DB->get_record_sql($sql, ['idnumber' => 'q1']);
|
||||
$args->itemid = $newquestion1->id;
|
||||
$commentobj = new \comment($args);
|
||||
$this->assertEquals($commentobj->count(), 2);
|
||||
|
||||
$newquestion2 = $DB->get_record('question', ['idnumber' => 'q2']);
|
||||
$newquestion2 = $DB->get_record_sql($sql, ['idnumber' => 'q2']);
|
||||
$args->itemid = $newquestion2->id;
|
||||
$commentobj = new \comment($args);
|
||||
$this->assertEquals($commentobj->count(), 1);
|
||||
|
|
|
@ -104,7 +104,7 @@ class behat_qbank_comment extends behat_question_base {
|
|||
|
||||
if ($this->running_javascript()) {
|
||||
$commentstextarea = $this->find('css',
|
||||
'.question-comment-view .comment-area textarea', $exception);
|
||||
'.comment-area textarea', $exception);
|
||||
$commentstextarea->setValue($comment);
|
||||
|
||||
// We delay 1 second which is all we need.
|
||||
|
@ -115,6 +115,31 @@ class behat_qbank_comment extends behat_question_base {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified comment from the current question comment preview.
|
||||
*
|
||||
* @Then I delete :arg comment from question preview
|
||||
* @param string $comment
|
||||
*/
|
||||
public function i_delete_comment_from_question_preview($comment) {
|
||||
|
||||
$exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment ');
|
||||
|
||||
// Using xpath liternal to avoid possible problems with comments containing quotes.
|
||||
$commentliteral = behat_context_helper::escape($comment);
|
||||
|
||||
$commentxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' comment-ctrl ')]" .
|
||||
"/descendant::div[@class='comment-message'][contains(., $commentliteral)]";
|
||||
|
||||
// Click on delete icon.
|
||||
$this->execute('behat_general::i_click_on_in_the',
|
||||
["Delete comment posted by", "icon", $this->escape($commentxpath), "xpath_element"]
|
||||
);
|
||||
|
||||
// Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
|
||||
$this->getSession()->wait(4 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified comment from the current question comment modal.
|
||||
*
|
||||
|
|
|
@ -17,7 +17,7 @@ Feature: A Teacher can comment in a question
|
|||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
|
@ -63,6 +63,7 @@ Feature: A Teacher can comment in a question
|
|||
And I navigate to "Question bank" in current page administration
|
||||
And I set the field "Select a category" to "Test questions"
|
||||
And I choose "Preview" action for "First question" in the question bank
|
||||
And I click on "Comments" "link"
|
||||
Then I should see "Save comment"
|
||||
And I add "Super test comment 01" comment to question preview
|
||||
And I click on "Save comment" "link"
|
||||
|
@ -71,7 +72,8 @@ Feature: A Teacher can comment in a question
|
|||
And I click on "Close preview" "button"
|
||||
Then I should see "1" on the comments column
|
||||
And I choose "Preview" action for "First question" in the question bank
|
||||
And I delete "Super test comment 01" comment from question
|
||||
And I click on "Comments" "link"
|
||||
And I delete "Super test comment 01" comment from question preview
|
||||
And I should not see "Super test comment 01"
|
||||
And I click on "Close preview" "button"
|
||||
Then I should see "0" on the comments column
|
||||
|
@ -100,6 +102,7 @@ Feature: A Teacher can comment in a question
|
|||
And I press "id_submitbutton"
|
||||
Then I should see "Essay 01 new"
|
||||
And I choose "Preview" action for "Essay 01 new" in the question bank
|
||||
And I click on "Comments" "link"
|
||||
Then I should see "Save comment"
|
||||
And I log out
|
||||
Then I log in as "teacher2"
|
||||
|
@ -112,3 +115,34 @@ Feature: A Teacher can comment in a question
|
|||
And I choose "Preview" action for "Essay 01 new" in the question bank
|
||||
Then I should not see "Save comment"
|
||||
And I click on "Close preview" "button"
|
||||
|
||||
@javascript
|
||||
Scenario: Comments added from the quiz page are visible
|
||||
Given I log in as "teacher1"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
When I navigate to "Edit quiz" in current page administration
|
||||
And I press "Add"
|
||||
And I follow "from question bank"
|
||||
And I click on "Select" "checkbox" in the "First question" "table_row"
|
||||
And I click on "Add selected questions to the quiz" "button"
|
||||
And I click on "Preview question" "link"
|
||||
And I switch to "questionpreview" window
|
||||
And I press "Comments"
|
||||
And I set the field "content" to "Some new comment"
|
||||
And I click on "Save comment" "link"
|
||||
And I should see "Some new comment"
|
||||
And I switch to the main window
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I choose "Preview" action for "First question" in the question bank
|
||||
And I click on "Comments" "link"
|
||||
And I should see "Some new comment"
|
||||
And I should see "T1 Teacher1"
|
||||
And I delete "Some new comment" comment from question preview
|
||||
And I should not see "Some new comment"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Edit quiz" in current page administration
|
||||
And I click on "Preview question" "link"
|
||||
And I switch to "questionpreview" window
|
||||
And I press "Comments"
|
||||
Then I should not see "Some new comment"
|
||||
|
|
|
@ -22,7 +22,6 @@ use comment;
|
|||
use context;
|
||||
use context_course;
|
||||
use core_question_generator;
|
||||
use question_edit_contexts;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
|
@ -66,7 +65,7 @@ class comment_created_deleted_test extends advanced_testcase {
|
|||
$this->context = context_course::instance($this->course->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$contexts = new question_edit_contexts($this->context);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($this->context);
|
||||
$cat = question_make_default_categories($contexts->all());
|
||||
$this->questiondata = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $cat->id]);
|
||||
|
|
|
@ -287,7 +287,12 @@ class qbank_customfields_customfield_testcase extends advanced_testcase {
|
|||
$this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id);
|
||||
|
||||
// The questions and their associated custom fields should have been restored.
|
||||
$newquestion1 = $DB->get_record('question', ['idnumber' => 'q1']);
|
||||
$sql = 'SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.idnumber = ?';
|
||||
$newquestion1 = $DB->get_record_sql($sql, ['q1']);
|
||||
$newquestion1cfdata = $customfieldhandler->export_instance_data_object($newquestion1->id);
|
||||
$this->assertEquals('some text', $newquestion1cfdata->f1);
|
||||
$this->assertEquals('Yes', $newquestion1cfdata->f2);
|
||||
|
@ -295,7 +300,7 @@ class qbank_customfields_customfield_testcase extends advanced_testcase {
|
|||
$this->assertEquals('b', $newquestion1cfdata->f4);
|
||||
$this->assertEquals('test', $newquestion1cfdata->f5);
|
||||
|
||||
$newquestion2 = $DB->get_record('question', ['idnumber' => 'q2']);
|
||||
$newquestion2 = $DB->get_record_sql($sql, ['q2']);
|
||||
$newquestion2cfdata = $customfieldhandler->export_instance_data_object($newquestion2->id);
|
||||
$this->assertEquals('some more text', $newquestion2cfdata->f1);
|
||||
$this->assertEquals('No', $newquestion2cfdata->f2);
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
|
||||
namespace qbank_deletequestion;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use core_question\local\bank\menu_action_column_base;
|
||||
|
||||
/**
|
||||
|
@ -82,7 +83,7 @@ class delete_action_column extends menu_action_column_base {
|
|||
if (!question_has_capability_on($question, 'edit')) {
|
||||
return [null, null, null];
|
||||
}
|
||||
if ($question->hidden) {
|
||||
if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) {
|
||||
$hiddenparams = array(
|
||||
'unhide' => $question->id,
|
||||
'sesskey' => sesskey());
|
||||
|
@ -102,7 +103,6 @@ class delete_action_column extends menu_action_column_base {
|
|||
|
||||
public function get_required_fields(): array {
|
||||
$required = parent::get_required_fields();
|
||||
$required[] = 'q.hidden';
|
||||
return $required;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ if ($cmid) {
|
|||
throw new moodle_exception('missingcourseorcmid', 'question');
|
||||
}
|
||||
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$url = new moodle_url('/question/bank/deletequestion/delete.php');
|
||||
|
||||
$PAGE->set_url($url);
|
||||
|
@ -62,7 +62,8 @@ $PAGE->set_heading($COURSE->fullname);
|
|||
// Unhide a question.
|
||||
if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
|
||||
question_require_capability_on($unhide, 'edit');
|
||||
$DB->set_field('question', 'hidden', 0, array('id' => $unhide));
|
||||
$DB->set_field('question_versions', 'status',
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_READY, ['questionid' => $unhide]);
|
||||
|
||||
// Purge these questions from the cache.
|
||||
\question_bank::notify_question_edited($unhide);
|
||||
|
@ -81,7 +82,8 @@ if ($deleteselected && ($confirm = optional_param('confirm', '', PARAM_ALPHANUM)
|
|||
$questionid = (int)$questionid;
|
||||
question_require_capability_on($questionid, 'edit');
|
||||
if (questions_in_use(array($questionid))) {
|
||||
$DB->set_field('question', 'hidden', 1, array('id' => $questionid));
|
||||
$DB->set_field('question_versions', 'status',
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $questionid]);
|
||||
} else {
|
||||
question_delete_question($questionid);
|
||||
}
|
||||
|
|
|
@ -55,7 +55,8 @@ Feature: Use the qbank plugin manager page for deletequestion
|
|||
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 click on "Select all" "checkbox"
|
||||
And I click on "First question" "checkbox"
|
||||
And I click on "First question second" "checkbox"
|
||||
And I click on "With selected" "button"
|
||||
And I click on question bulk action "deleteselected"
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
|
|
2
question/bank/editquestion/amd/build/question_status.min.js
vendored
Normal file
2
question/bank/editquestion/amd/build/question_status.min.js
vendored
Normal file
|
@ -0,0 +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)})}});
|
||||
//# sourceMappingURL=question_status.min.js.map
|
File diff suppressed because one or more lines are too long
135
question/bank/editquestion/amd/src/question_status.js
Normal file
135
question/bank/editquestion/amd/src/question_status.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
// 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/>.
|
||||
|
||||
/**
|
||||
* Status column selector js.
|
||||
*
|
||||
* @module qbank_editquestion/question_status
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the question status.
|
||||
*
|
||||
* @param {Number} questionId The question id.
|
||||
* @param {String} formData The question tag form data in a URI encoded param string
|
||||
* @return {Array} The modified question status
|
||||
*/
|
||||
const setQuestionStatus = (questionId, formData) => Ajax.call([{
|
||||
methodname: 'qbank_editquestion_set_status',
|
||||
args: {
|
||||
questionid: questionId,
|
||||
formdata: formData
|
||||
}
|
||||
}])[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.
|
||||
*/
|
||||
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);
|
||||
});
|
||||
};
|
|
@ -27,6 +27,8 @@
|
|||
|
||||
namespace qbank_editquestion;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
|
||||
/**
|
||||
* Class editquestion_helper for methods related to add/edit/copy
|
||||
*
|
||||
|
@ -83,4 +85,27 @@ class editquestion_helper {
|
|||
}
|
||||
return $PAGE->get_renderer('qbank_editquestion')->render_create_new_question_button($addquestiondisplay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string for the status of the question.
|
||||
*
|
||||
* @param string $status
|
||||
* @return string
|
||||
*/
|
||||
public static function get_question_status_string($status): string {
|
||||
return get_string('questionstatus' . $status, 'qbank_editquestion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of status of the questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_question_status_list(): array {
|
||||
$statuslist = [];
|
||||
$statuslist[question_version_status::QUESTION_STATUS_READY] = get_string('questionstatusready', 'qbank_editquestion');
|
||||
$statuslist[question_version_status::QUESTION_STATUS_DRAFT] = get_string('questionstatusdraft', 'qbank_editquestion');
|
||||
return $statuslist;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
109
question/bank/editquestion/classes/external/update_question_version_status.php
vendored
Normal file
109
question/bank/editquestion/classes/external/update_question_version_status.php
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?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\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->libdir . '/externallib.php');
|
||||
require_once($CFG->dirroot . '/question/engine/bank.php');
|
||||
|
||||
use external_api;
|
||||
use external_function_parameters;
|
||||
use external_single_structure;
|
||||
use external_value;
|
||||
use qbank_editquestion\editquestion_helper;
|
||||
use question_bank;
|
||||
|
||||
/**
|
||||
* Update question status external api.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class update_question_version_status extends \external_api {
|
||||
|
||||
/**
|
||||
* Returns description of method parameters.
|
||||
*
|
||||
* @return external_function_parameters.
|
||||
*/
|
||||
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'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array The created or modified question tag
|
||||
*/
|
||||
public static function execute($questionid, $formdata) {
|
||||
global $DB;
|
||||
|
||||
$data = [];
|
||||
$result = [
|
||||
'status' => false,
|
||||
'statusname' => ''
|
||||
];
|
||||
|
||||
// Parameter validation.
|
||||
$params = self::validate_parameters(self::execute_parameters(), [
|
||||
'questionid' => $questionid,
|
||||
'formdata' => $formdata
|
||||
]);
|
||||
|
||||
parse_str($params['formdata'], $data);
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method result value.
|
||||
*/
|
||||
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')
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?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 <safatshahin@catalyst-au.net>
|
||||
* @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());
|
||||
}
|
||||
}
|
|
@ -57,4 +57,14 @@ class renderer extends \plugin_renderer_base {
|
|||
return $this->render_from_template('qbank_editquestion/add_new_question', $addquestiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render question information for edit form.
|
||||
*
|
||||
* @param array $questiondata
|
||||
* @return bool|string
|
||||
*/
|
||||
public function render_question_info($questiondata) {
|
||||
return $this->render_from_template('qbank_editquestion/question_info', $questiondata);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ class plugin_feature extends \core_question\local\bank\plugin_features_base{
|
|||
public function get_question_columns($qbank): array {
|
||||
return [
|
||||
new edit_action_column($qbank),
|
||||
new copy_action_column($qbank)
|
||||
new copy_action_column($qbank),
|
||||
new question_status_column($qbank)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?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;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
use core_question\local\bank\question_version_status;
|
||||
|
||||
/**
|
||||
* A column to show the status of the question.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_status_column extends column_base {
|
||||
|
||||
public function get_name(): string {
|
||||
return 'questionstatus';
|
||||
}
|
||||
|
||||
protected function get_title(): string {
|
||||
return get_string('questionstatus', 'qbank_editquestion');
|
||||
}
|
||||
|
||||
protected function display_content($question, $rowclasses): void {
|
||||
global $PAGE;
|
||||
$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' => '#'
|
||||
];
|
||||
}
|
||||
echo \html_writer::tag('a', editquestion_helper::get_question_status_string($question->status), $attributes);
|
||||
}
|
||||
|
||||
}
|
|
@ -15,24 +15,21 @@
|
|||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Definition of question/type/random scheduled tasks.
|
||||
* External services definition for qbank_editquestion.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @category task
|
||||
* @copyright 2018 Bo Pierce <email.bO.pierce@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @package qbank_editquestion
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$tasks = array(
|
||||
array(
|
||||
'classname' => 'qtype_random\task\remove_unused_questions',
|
||||
'blocking' => 0,
|
||||
'minute' => 'R',
|
||||
'hour' => '*',
|
||||
'day' => '*',
|
||||
'month' => '*',
|
||||
'dayofweek' => '*'
|
||||
)
|
||||
);
|
||||
$functions = [
|
||||
'qbank_editquestion_set_status' => [
|
||||
'classname' => 'qbank_editquestion\external\update_question_version_status',
|
||||
'description' => 'Update the question status.',
|
||||
'type' => 'write',
|
||||
'ajax' => true,
|
||||
],
|
||||
];
|
|
@ -25,3 +25,14 @@
|
|||
|
||||
$string['pluginname'] = 'Edit questions';
|
||||
$string['privacy:metadata'] = 'The Edit questions question bank plugin does not store any user data.';
|
||||
|
||||
// Question status.
|
||||
$string['questionstatus'] = 'Status';
|
||||
$string['questionstatusready'] = 'Ready';
|
||||
$string['questionstatushidden'] = 'Hidden';
|
||||
$string['questionstatusdraft'] = 'Draft';
|
||||
$string['questionstatusheader'] = 'Change question status';
|
||||
|
||||
// Edit form.
|
||||
$string['versioninfo'] = 'Version';
|
||||
$string['status'] = 'Question status';
|
||||
|
|
41
question/bank/editquestion/lib.php
Normal file
41
question/bank/editquestion/lib.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?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 <safatshahin@catalyst-au.net>
|
||||
* @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();
|
||||
}
|
|
@ -108,7 +108,7 @@ if ($cmid) {
|
|||
} else {
|
||||
throw new moodle_exception('missingcourseorcmid', 'question');
|
||||
}
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$PAGE->set_pagelayout('admin');
|
||||
|
||||
if (optional_param('addcancel', false, PARAM_BOOL)) {
|
||||
|
@ -174,10 +174,12 @@ if ($id) {
|
|||
if (!$formeditable) {
|
||||
question_require_capability_on($question, 'view');
|
||||
}
|
||||
$question->beingcopied = false;
|
||||
if ($makecopy) {
|
||||
// If we are duplicating a question, add some indication to the question name.
|
||||
$question->name = get_string('questionnamecopy', 'question', $question->name);
|
||||
$question->idnumber = core_question_find_next_unused_idnumber($question->idnumber, $category->id);
|
||||
$question->idnumber = isset($question->idnumber) ?
|
||||
core_question_find_next_unused_idnumber($question->idnumber, $category->id) : '';
|
||||
$question->beingcopied = true;
|
||||
}
|
||||
|
||||
|
@ -210,6 +212,17 @@ if ($formeditable && $id) {
|
|||
$toform->appendqnumstring = $appendqnumstring;
|
||||
$toform->returnurl = $originalreturnurl;
|
||||
$toform->makecopy = $makecopy;
|
||||
$toform->idnumber = null;
|
||||
if (isset($question->id)) {
|
||||
$questionobject = question_bank::load_question($question->id);
|
||||
$toform->status = $questionobject->status;
|
||||
$toform->idnumber = $questionobject->idnumber;
|
||||
} else {
|
||||
$toform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
}
|
||||
if ($makecopy) {
|
||||
$toform->idnumber = core_question_find_next_unused_idnumber($toform->idnumber, $category->id);
|
||||
}
|
||||
if ($cm !== null) {
|
||||
$toform->cmid = $cm->id;
|
||||
$toform->courseid = $cm->course;
|
||||
|
@ -236,7 +249,13 @@ if ($mform->is_cancelled()) {
|
|||
// If we are saving as a copy, break the connection to the old question.
|
||||
if ($makecopy) {
|
||||
$question->id = 0;
|
||||
$question->hidden = 0; // Copies should not be hidden.
|
||||
// Copies should not be hidden.
|
||||
$question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
}
|
||||
|
||||
// If is will be added directly to a module send the module name to be referenced.
|
||||
if ($appendqnumstring && $cm) {
|
||||
$fromform->modulename = 'mod_' . $cm->modname;
|
||||
}
|
||||
|
||||
// Process the combination of usecurrentcat, categorymoveto and category form
|
||||
|
@ -248,7 +267,7 @@ if ($mform->is_cancelled()) {
|
|||
// If we are moving a question, check we have permission to move it from
|
||||
// whence it came (Where we are moving to is validated by the form).
|
||||
list($newcatid, $newcontextid) = explode(',', $fromform->category);
|
||||
if (!empty($question->id) && $newcatid != $question->category) {
|
||||
if (!empty($question->id) && $newcatid != $question->categoryobject->id) {
|
||||
$contextid = $newcontextid;
|
||||
question_require_capability_on($question, 'move');
|
||||
} else {
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
{{!
|
||||
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_viewcreator/modifier_display
|
||||
@template qbank_editquestion/question_info
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"displaydata": [
|
||||
{
|
||||
"modifier": "Marc Ghaly",
|
||||
"date": "2 June 2021, 5:32 PM",
|
||||
}
|
||||
"elements": [
|
||||
{"pluginhtml":"<div>Version: 1</div>"},
|
||||
{"pluginhtml":"<div>Usage: 1</div>"}
|
||||
]
|
||||
}
|
||||
}}
|
||||
|
||||
<span class="qbank-creator-name">
|
||||
{{modifier}}
|
||||
</span>
|
||||
<br>
|
||||
<span class="date">
|
||||
{{date}}
|
||||
</span>
|
||||
<div class="question-edit-elements">
|
||||
{{#editelements}}
|
||||
{{{pluginhtml}}}
|
||||
{{/editelements}}
|
||||
</div>
|
|
@ -0,0 +1,39 @@
|
|||
@qbank @qbank_editquestion
|
||||
Feature: Use the qbank plugin manager page for editquestion
|
||||
In order to check the plugin behaviour with enable and disable
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| 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 |
|
||||
| Test questions | truefalse | First question second | Answer the first question |
|
||||
|
||||
Scenario: Enable/disable edit question columns from the base view
|
||||
Given I log in as "admin"
|
||||
When I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
|
||||
And I should see "Edit question"
|
||||
And I click on "Disable" "link" in the "Edit question" "table_row"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
Then I should not see "Status"
|
||||
And I click on ".dropdown-toggle" "css_element" in the "First question" "table_row"
|
||||
And I should not see "Edit question" in the "region-main" "region"
|
||||
And I should not see "Duplicate" in the "region-main" "region"
|
||||
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
|
||||
And I click on "Enable" "link" in the "Edit question" "table_row"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank > Questions" in current page administration
|
||||
And I click on ".dropdown-toggle" "css_element" in the "First question" "table_row"
|
||||
Then I should see "Status"
|
||||
And I click on ".dropdown-toggle" "css_element" in the "First question" "table_row"
|
||||
And I should see "Edit question" in the "region-main" "region"
|
||||
And I should see "Duplicate" in the "region-main" "region"
|
|
@ -0,0 +1,31 @@
|
|||
@qbank @qbank_editquestion
|
||||
Feature: Use the qbank base view to test the status change using
|
||||
the pop up
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Test quiz | C1 | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| 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 |
|
||||
|
||||
@javascript
|
||||
Scenario: Question status modal 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"
|
91
question/bank/editquestion/tests/external/update_question_version_status_test.php
vendored
Normal file
91
question/bank/editquestion/tests/external/update_question_version_status_test.php
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?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;
|
||||
|
||||
use qbank_editquestion\external\update_question_version_status;
|
||||
|
||||
/**
|
||||
* Submit status external api test.
|
||||
*
|
||||
* @package qbank_editquestion
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @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\editquestion_helper
|
||||
*/
|
||||
class update_question_version_status_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Called before every test.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $USER;
|
||||
parent::setUp();
|
||||
$this->setAdminUser();
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->user = $USER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the submit status webservice changes the status of the question.
|
||||
*
|
||||
* @covers ::mock_generate_submit_keys
|
||||
* @covers ::execute
|
||||
* @covers ::get_question_status_string
|
||||
*/
|
||||
public function test_submit_status_updates_the_question_status() {
|
||||
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']);
|
||||
$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, '', '&'));
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
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']);
|
||||
$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, '', '&'));
|
||||
$countafterupdate = $DB->count_records('question_versions');
|
||||
$this->assertEquals($countcurrentrecords, $countafterupdate);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,6 @@
|
|||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qbank_editquestion';
|
||||
$plugin->version = 2021062800;
|
||||
$plugin->version = 2021110800;
|
||||
$plugin->requires = 2021052500;
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
|
|
@ -50,7 +50,7 @@ if ($cmid) {
|
|||
require_sesskey();
|
||||
|
||||
// Load the necessary data.
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$questiondata = question_bank::load_question_data($questionid);
|
||||
|
||||
// Check permissions.
|
||||
|
|
|
@ -51,7 +51,7 @@ $category->context = $categorycontext;
|
|||
// This page can be called without courseid or cmid in which case.
|
||||
// We get the context from the category object.
|
||||
if ($contexts === null) { // Need to get the course from the chosen category.
|
||||
$contexts = new question_edit_contexts($categorycontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($categorycontext);
|
||||
$thiscontext = $contexts->lowest();
|
||||
if ($thiscontext->contextlevel == CONTEXT_COURSE) {
|
||||
require_login($thiscontext->instanceid, false);
|
||||
|
|
|
@ -92,6 +92,8 @@ if ($param->moveupcontext || $param->movedowncontext) {
|
|||
$category->contextid = $param->tocontext;
|
||||
$event = \core\event\question_category_moved::create_from_question_category_instance($category);
|
||||
$event->trigger();
|
||||
// Update the set_reference records when moving a category to a different context.
|
||||
move_question_set_references($catid, $catid, $oldcat->contextid, $category->contextid);
|
||||
$qcobject->update_category($catid, "{$newtopcat->id},{$param->tocontext}", $oldcat->name, $oldcat->info);
|
||||
// The previous line does a redirect().
|
||||
}
|
||||
|
@ -102,7 +104,8 @@ if ($param->delete) {
|
|||
}
|
||||
|
||||
helper::question_remove_stale_questions_from_category($param->delete);
|
||||
$questionstomove = $DB->count_records("question", ["category" => $param->delete]);
|
||||
|
||||
$questionstomove = $DB->count_records('question_bank_entries', ['questioncategoryid' => $param->delete]);
|
||||
|
||||
// Second pass, if we still have questions to move, setup the form.
|
||||
if ($questionstomove) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
namespace qbank_managecategories;
|
||||
|
||||
use context;
|
||||
use core_question\local\bank\question_version_status;
|
||||
use moodle_exception;
|
||||
use html_writer;
|
||||
|
||||
|
@ -60,14 +61,19 @@ class helper {
|
|||
public static function question_remove_stale_questions_from_category(int $categoryid): void {
|
||||
global $DB;
|
||||
|
||||
$select = 'category = :categoryid AND (qtype = :qtype OR hidden = :hidden)';
|
||||
$params = ['categoryid' => $categoryid, 'qtype' => 'random', 'hidden' => 1];
|
||||
$questions = $DB->get_recordset_select("question", $select, $params, '', 'id');
|
||||
$sql = "SELECT q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = :categoryid
|
||||
AND (q.qtype = :qtype OR qv.status = :status)";
|
||||
|
||||
$params = ['categoryid' => $categoryid, 'qtype' => 'random', 'status' => question_version_status::QUESTION_STATUS_HIDDEN];
|
||||
$questions = $DB->get_records_sql($sql, $params);
|
||||
foreach ($questions as $question) {
|
||||
// The function question_delete_question does not delete questions in use.
|
||||
question_delete_question($question->id);
|
||||
}
|
||||
$questions->close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -240,22 +246,41 @@ class helper {
|
|||
* Get all the category objects, including a count of the number of questions in that category,
|
||||
* for all the categories in the lists $contexts.
|
||||
*
|
||||
* @param mixed $contexts either a single contextid, or a comma-separated list of context ids.
|
||||
* @param context $contexts
|
||||
* @param string $sortorder used as the ORDER BY clause in the select statement.
|
||||
* @param bool $top Whether to return the top categories or not.
|
||||
* @param int $showallversions 1 to show all versions not only the latest.
|
||||
* @return array of category objects.
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function get_categories_for_contexts($contexts, string $sortorder = 'parent, sortorder, name ASC',
|
||||
bool $top = false): array {
|
||||
bool $top = false, int $showallversions = 0): array {
|
||||
global $DB;
|
||||
$topwhere = $top ? '' : 'AND c.parent <> 0';
|
||||
return $DB->get_records_sql("
|
||||
SELECT c.*, (SELECT count(1) FROM {question} q
|
||||
WHERE c.id = q.category AND q.hidden='0' AND q.parent='0') AS questioncount
|
||||
FROM {question_categories} c
|
||||
WHERE c.contextid IN ($contexts) $topwhere
|
||||
ORDER BY $sortorder");
|
||||
$statuscondition = "AND (qv.status = '". question_version_status::QUESTION_STATUS_READY . "' " .
|
||||
" OR qv.status = '" . question_version_status::QUESTION_STATUS_DRAFT . "' )";
|
||||
|
||||
$sql = "SELECT c.*,
|
||||
(SELECT COUNT(1)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE q.parent = '0'
|
||||
$statuscondition
|
||||
AND c.id = qbe.questioncategoryid
|
||||
AND ($showallversions = 1
|
||||
OR (qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qbe.id)
|
||||
)
|
||||
)
|
||||
) AS questioncount
|
||||
FROM {question_categories} c
|
||||
WHERE c.contextid IN ($contexts) $topwhere
|
||||
ORDER BY $sortorder";
|
||||
|
||||
return $DB->get_records_sql($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -328,8 +328,15 @@ class question_category_object {
|
|||
*/
|
||||
public function move_questions(int $oldcat, int $newcat): void {
|
||||
global $DB;
|
||||
$questionids = $DB->get_records_select_menu('question',
|
||||
'category = ? AND (parent = 0 OR parent = id)', [$oldcat], '', 'id,1');
|
||||
|
||||
$sql = "SELECT q.id, 1
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?
|
||||
AND (q.parent = 0 OR q.parent = q.id)";
|
||||
|
||||
$questionids = $DB->get_records_sql_menu($sql, [$oldcat]);
|
||||
question_move_questions_to_category(array_keys($questionids), $newcat);
|
||||
}
|
||||
|
||||
|
@ -479,14 +486,25 @@ class question_category_object {
|
|||
|
||||
// If the category name has changed, rename any random questions in that category.
|
||||
if ($oldcat->name != $cat->name) {
|
||||
$where = "qtype = 'random' AND category = ? AND " . $DB->sql_compare_text('questiontext') . " = ?";
|
||||
// Get the question ids for each question category.
|
||||
$sql = "SELECT q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?";
|
||||
|
||||
$randomqtype = question_bank::get_qtype('random');
|
||||
$randomqname = $randomqtype->question_name($cat, false);
|
||||
$DB->set_field_select('question', 'name', $randomqname, $where, [$cat->id, '0']);
|
||||
$questionids = $DB->get_records_sql($sql, [$cat->id]);
|
||||
|
||||
$randomqname = $randomqtype->question_name($cat, true);
|
||||
$DB->set_field_select('question', 'name', $randomqname, $where, [$cat->id, '1']);
|
||||
foreach ($questionids as $question) {
|
||||
$where = "qtype = 'random' AND id = ? AND " . $DB->sql_compare_text('questiontext') . " = ?";
|
||||
|
||||
$randomqtype = question_bank::get_qtype('random');
|
||||
$randomqname = $randomqtype->question_name($cat, false);
|
||||
$DB->set_field_select('question', 'name', $randomqname, $where, [$question->id, '0']);
|
||||
|
||||
$randomqname = $randomqtype->question_name($cat, true);
|
||||
$DB->set_field_select('question', 'name', $randomqname, $where, [$question->id, '1']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($oldcat->contextid != $tocontextid) {
|
||||
|
|
|
@ -72,52 +72,73 @@ class helper_test extends \advanced_testcase {
|
|||
|
||||
$qcat1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$q1a = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat1->id]); // Will be hidden.
|
||||
$DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
|
||||
$DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $q1a->id]);
|
||||
|
||||
$qcat2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$q2a = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden.
|
||||
$q2b = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden but used.
|
||||
$DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
|
||||
$DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
|
||||
$DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $q2a->id]);
|
||||
$DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $q2b->id]);
|
||||
quiz_add_quiz_question($q2b->id, $this->quiz);
|
||||
|
||||
// Adding a new random question does not add a new question, adds a question_set_references record.
|
||||
quiz_add_random_questions($this->quiz, 0, $qcat2->id, 1, false);
|
||||
|
||||
// We added one random question to the quiz and we expect the quiz to have only one random question.
|
||||
$q2d = $DB->get_record_sql("SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {quiz_slots} s ON s.questionid = q.id
|
||||
WHERE q.qtype = :qtype
|
||||
AND s.quizid = :quizid",
|
||||
['qtype' => 'random', 'quizid' => $this->quiz->id], MUST_EXIST);
|
||||
$q2d = $DB->get_record_sql("SELECT qsr.*
|
||||
FROM {quiz_slots} qs
|
||||
JOIN {question_set_references} qsr ON qsr.itemid = qs.id
|
||||
WHERE qs.quizid = ?",
|
||||
['quizid' => $this->quiz->id], MUST_EXIST);
|
||||
|
||||
// The following 2 lines have to be after the quiz_add_random_questions() call above.
|
||||
// Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
|
||||
$q1b = $this->qgenerator->create_question('random', null, ['category' => $qcat1->id]); // Will not be used.
|
||||
$q2c = $this->qgenerator->create_question('random', null, ['category' => $qcat2->id]); // Will not be used.
|
||||
|
||||
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
|
||||
$sql = "SELECT count(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?";
|
||||
$this->assertEquals(2, $DB->count_records_sql($sql, [$qcat1->id]));
|
||||
$this->assertEquals(3, $DB->count_records_sql($sql, [$qcat2->id]));
|
||||
|
||||
// Non-existing category, nothing will happen.
|
||||
helper::question_remove_stale_questions_from_category(0);
|
||||
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
|
||||
$sql = "SELECT count(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?";
|
||||
$this->assertEquals(2, $DB->count_records_sql($sql, [$qcat1->id]));
|
||||
$this->assertEquals(3, $DB->count_records_sql($sql, [$qcat2->id]));
|
||||
|
||||
// First category, should be empty afterwards.
|
||||
helper::question_remove_stale_questions_from_category($qcat1->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
|
||||
$this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
|
||||
$sql = "SELECT count(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?";
|
||||
$this->assertEquals(0, $DB->count_records_sql($sql, [$qcat1->id]));
|
||||
$this->assertEquals(3, $DB->count_records_sql($sql, [$qcat2->id]));
|
||||
$this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
|
||||
$this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
|
||||
|
||||
// Second category, used questions should be left untouched.
|
||||
helper::question_remove_stale_questions_from_category($qcat2->id);
|
||||
$this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
|
||||
$this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
|
||||
$sql = "SELECT count(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?";
|
||||
$this->assertEquals(0, $DB->count_records_sql($sql, [$qcat1->id]));
|
||||
$this->assertEquals(1, $DB->count_records_sql($sql, [$qcat2->id]));
|
||||
$this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
|
||||
$this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
|
||||
$this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
|
||||
$this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
|
||||
$this->assertTrue($DB->record_exists('question_set_references', ['id' => $q2d->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -184,7 +205,7 @@ class helper_test extends \advanced_testcase {
|
|||
public function test_question_category_select_menu() {
|
||||
|
||||
$this->qgenerator->create_question_category(['contextid' => $this->context->id, 'name' => 'Test this question category']);
|
||||
$contexts = new \question_edit_contexts($this->context);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($this->context);
|
||||
|
||||
ob_start();
|
||||
helper::question_category_select_menu($contexts->having_cap('moodle/question:add'));
|
||||
|
@ -210,7 +231,7 @@ class helper_test extends \advanced_testcase {
|
|||
$qcategory2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id, 'parent' => $qcategory1->id]);
|
||||
$qcategory3 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
|
||||
$contexts = new \question_edit_contexts($this->context);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($this->context);
|
||||
|
||||
// Validate that we have the array with the categories tree.
|
||||
$categorycontexts = helper::question_category_options($contexts->having_cap('moodle/question:add'));
|
||||
|
|
|
@ -25,7 +25,7 @@ use context;
|
|||
use context_course;
|
||||
use context_module;
|
||||
use moodle_url;
|
||||
use question_edit_contexts;
|
||||
use core_question\local\bank\question_edit_contexts;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
define ("qbank_previewquestion/preview",["exports","jquery"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);a.init=function init(a){if(!a){var b=document.getElementById("close-previewquestion-page");b.onclick=function(){window.close()}}c("responseform")};var c=function(a){var b=document.getElementById(a);if(b){d(b);e(b);f(".questionflagsavebutton",b);g(b)}},d=function(a){a.setAttribute("autocomplete","off")},e=function(a){a.addEventListener("submit",function(){(0,b.default)(this).submit(function(){return!1});return!0})},f=function(a,b){b.querySelectorAll(a).forEach(function(a){return a.remove()})},g=function(a){var b=window.location.href.match(/^.*[?&]scrollpos=(\d*)(?:&|$|#).*$/,"$1");if(b){window.scrollTo(0,b[1]);a.addEventListener("DOMContentLoaded",function(){window.scrollTo(0,b[1])})}}});
|
||||
define ("qbank_previewquestion/preview",["exports","jquery"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);a.init=function init(a,b){if(!a){var d=document.getElementById("close-previewquestion-page");d.onclick=function(){if(null===window.opener){location.href=b}else{window.close()}}}c("responseform")};var c=function(a){var b=document.getElementById(a);if(b){d(b);e(b);f(".questionflagsavebutton",b);g(b)}},d=function(a){a.setAttribute("autocomplete","off")},e=function(a){a.addEventListener("submit",function(){(0,b.default)(this).submit(function(){return!1});return!0})},f=function(a,b){b.querySelectorAll(a).forEach(function(a){return a.remove()})},g=function(a){var b=window.location.href.match(/^.*[?&]scrollpos=(\d*)(?:&|$|#).*$/,"$1");if(b){window.scrollTo(0,b[1]);a.addEventListener("DOMContentLoaded",function(){window.scrollTo(0,b[1])})}}});
|
||||
//# sourceMappingURL=preview.min.js.map
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -29,12 +29,17 @@ import $ from 'jquery';
|
|||
*
|
||||
* @method init
|
||||
* @param {bool} redirect Redirect.
|
||||
* @param {string} url url to redirect.
|
||||
*/
|
||||
export const init = (redirect) => {
|
||||
export const init = (redirect, url) => {
|
||||
if (!redirect) {
|
||||
let closeButton = document.getElementById('close-previewquestion-page');
|
||||
closeButton.onclick = () => {
|
||||
window.close();
|
||||
if (window.opener === null) {
|
||||
location.href = url;
|
||||
} else {
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
// Set up the form to be displayed.
|
||||
|
|
|
@ -42,8 +42,12 @@ class preview_options_form extends moodleform {
|
|||
question_display_options::VISIBLE => get_string('shown', 'question'),
|
||||
];
|
||||
|
||||
$mform->addElement('header', 'attemptoptionsheader', get_string('attemptoptions', 'question'));
|
||||
|
||||
$mform->addElement('header', 'attemptoptionsheader', get_string('previewoptions', 'qbank_previewquestion'));
|
||||
$mform->setExpanded('attemptoptionsheader', false);
|
||||
$versions = $this->_customdata['versions'];
|
||||
$currentversion = $this->_customdata['questionversion'];
|
||||
$select = $mform->addElement('select', 'version', get_string('questionversion', 'qbank_previewquestion'), $versions);
|
||||
$select->setSelected($currentversion);
|
||||
$behaviours = question_engine::get_behaviour_options(
|
||||
$this->_customdata['quba']->get_preferred_behaviour());
|
||||
$mform->addElement('select', 'behaviour',
|
||||
|
@ -63,6 +67,7 @@ class preview_options_form extends moodleform {
|
|||
get_string('restartwiththeseoptions', 'question'));
|
||||
|
||||
$mform->addElement('header', 'displayoptionsheader', get_string('displayoptions', 'question'));
|
||||
$mform->setExpanded('displayoptionsheader', false);
|
||||
|
||||
$mform->addElement('select', 'correctness', get_string('whethercorrect', 'question'),
|
||||
$hiddenorvisible);
|
||||
|
|
|
@ -16,8 +16,21 @@
|
|||
|
||||
namespace qbank_previewquestion;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/editlib.php');
|
||||
|
||||
use action_menu;
|
||||
use comment;
|
||||
use context_module;
|
||||
use context;
|
||||
use core\plugininfo\qbank;
|
||||
use core_question\local\bank\edit_menu_column;
|
||||
use core_question\local\bank\view;
|
||||
use core_question\local\bank\question_edit_contexts;
|
||||
use moodle_url;
|
||||
use question_bank;
|
||||
use question_definition;
|
||||
use question_display_options;
|
||||
use question_engine;
|
||||
use stdClass;
|
||||
|
@ -44,7 +57,7 @@ class helper {
|
|||
* @param int $slot the relevant slot within the usage.
|
||||
* @param array $args the remaining bits of the file path.
|
||||
* @param bool $forcedownload whether the user must be forced to download the file.
|
||||
* @param array $fileoptions
|
||||
* @param array $fileoptions options for the stored files
|
||||
* @return void false if file not found, does not return if found - justsend the file
|
||||
*/
|
||||
public static function question_preview_question_pluginfile($course, $context, $component,
|
||||
|
@ -85,11 +98,11 @@ class helper {
|
|||
/**
|
||||
* The the URL to use for actions relating to this preview.
|
||||
*
|
||||
* @param int $questionid the question being previewed.
|
||||
* @param int $qubaid the id of the question usage for this preview.
|
||||
* @param question_preview_options $options the options in use.
|
||||
* @param context $context
|
||||
* @param moodle_url $returnurl
|
||||
* @param int $questionid the question being previewed
|
||||
* @param int $qubaid the id of the question usage for this preview
|
||||
* @param question_preview_options $options the options in use
|
||||
* @param context $context context for the question preview
|
||||
* @param moodle_url $returnurl url of the page to return to
|
||||
* @return moodle_url
|
||||
*/
|
||||
public static function question_preview_action_url($questionid, $qubaid,
|
||||
|
@ -112,10 +125,11 @@ class helper {
|
|||
|
||||
/**
|
||||
* The the URL to use for actions relating to this preview.
|
||||
* @param int $questionid the question being previewed.
|
||||
* @param context $context the current moodle context.
|
||||
* @param int $previewid optional previewid to sign post saved previewed answers.
|
||||
* @param moodle_url $returnurl
|
||||
*
|
||||
* @param int $questionid the question being previewed
|
||||
* @param context $context the current moodle context
|
||||
* @param int $previewid optional previewid to sign post saved previewed answers
|
||||
* @param moodle_url $returnurl url of the page to return to
|
||||
* @return moodle_url
|
||||
*/
|
||||
public static function question_preview_form_url($questionid, $context, $previewid = null, $returnurl = null): moodle_url {
|
||||
|
@ -138,13 +152,16 @@ class helper {
|
|||
|
||||
/**
|
||||
* Delete the current preview, if any, and redirect to start a new preview.
|
||||
* @param int $previewid
|
||||
* @param int $questionid
|
||||
* @param object $displayoptions
|
||||
* @param object $context
|
||||
* @param moodle_url $returnurl
|
||||
*
|
||||
* @param int $previewid id of the preview while restarting it
|
||||
* @param int $questionid id of the question in preview
|
||||
* @param object $displayoptions display options for the question in preview
|
||||
* @param object $context context of the question for preview
|
||||
* @param moodle_url $returnurl url of the page to return to
|
||||
* @param int|null $version version of the question in preview
|
||||
*/
|
||||
public static function restart_preview($previewid, $questionid, $displayoptions, $context, $returnurl = null): void {
|
||||
public static function restart_preview($previewid, $questionid, $displayoptions, $context,
|
||||
$returnurl = null, $version = null): void {
|
||||
global $DB;
|
||||
|
||||
if ($previewid) {
|
||||
|
@ -153,27 +170,33 @@ class helper {
|
|||
$transaction->allow_commit();
|
||||
}
|
||||
redirect(self::question_preview_url($questionid, $displayoptions->behaviour,
|
||||
$displayoptions->maxmark, $displayoptions, $displayoptions->variant, $context, $returnurl));
|
||||
$displayoptions->maxmark, $displayoptions, $displayoptions->variant, $context, $returnurl, $version));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the URL for starting a new preview of a given question with the given options.
|
||||
* @param integer $questionid the question to preview.
|
||||
* @param string $preferredbehaviour the behaviour to use for the preview.
|
||||
* @param float $maxmark the maximum to mark the question out of.
|
||||
* @param question_display_options $displayoptions the display options to use.
|
||||
*
|
||||
* @param integer $questionid the question to preview
|
||||
* @param string $preferredbehaviour the behaviour to use for the preview
|
||||
* @param float $maxmark the maximum to mark the question out of
|
||||
* @param question_display_options $displayoptions the display options to use
|
||||
* @param int $variant the variant of the question to preview. If null, one will
|
||||
* be picked randomly.
|
||||
* be picked randomly
|
||||
* @param object $context context to run the preview in (affects things like
|
||||
* filter settings, theme, lang, etc.) Defaults to $PAGE->context.
|
||||
* @param moodle_url $returnurl
|
||||
* @return moodle_url the URL.
|
||||
* filter settings, theme, lang, etc.) Defaults to $PAGE->context
|
||||
* @param moodle_url $returnurl url of the page to return to
|
||||
* @param int $version version of the question
|
||||
* @return moodle_url the URL
|
||||
*/
|
||||
public static function question_preview_url($questionid, $preferredbehaviour = null,
|
||||
$maxmark = null, $displayoptions = null, $variant = null, $context = null, $returnurl = null): moodle_url {
|
||||
$maxmark = null, $displayoptions = null, $variant = null, $context = null, $returnurl = null,
|
||||
$version = null): moodle_url {
|
||||
|
||||
$params = ['id' => $questionid];
|
||||
|
||||
if (!is_null($version)) {
|
||||
$params['id'] = $version;
|
||||
}
|
||||
if (is_null($context)) {
|
||||
global $PAGE;
|
||||
$context = $PAGE->context;
|
||||
|
@ -215,6 +238,7 @@ class helper {
|
|||
|
||||
/**
|
||||
* Popup params for the question preview.
|
||||
*
|
||||
* @return array that can be passed as $params to the {@see popup_action} constructor.
|
||||
*/
|
||||
public static function question_preview_popup_params(): array {
|
||||
|
@ -227,11 +251,11 @@ class helper {
|
|||
/**
|
||||
* Get the extra elements for preview from qbank plugins.
|
||||
*
|
||||
* @param \question_definition $question
|
||||
* @param int $courseid
|
||||
* @param question_definition $question question definition object
|
||||
* @param int $courseid id of the course
|
||||
* @return array
|
||||
*/
|
||||
public static function get_preview_extra_elements(\question_definition $question, int $courseid): array {
|
||||
public static function get_preview_extra_elements(question_definition $question, int $courseid): array {
|
||||
$plugintype = 'qbank';
|
||||
$functionname = 'preview_display';
|
||||
$extrahtml = [];
|
||||
|
@ -247,4 +271,46 @@ class helper {
|
|||
}
|
||||
return [$comment, $extrahtml];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if question is the latest version.
|
||||
*
|
||||
* @param string $version Question version to check
|
||||
* @param string $questionbankentryid Entry to check against
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_latest(string $version, string $questionbankentryid) : bool {
|
||||
global $DB;
|
||||
|
||||
$sql = 'SELECT MAX(version) AS max
|
||||
FROM {question_versions}
|
||||
WHERE questionbankentryid = ?';
|
||||
$latestversion = $DB->get_record_sql($sql, [$questionbankentryid]);
|
||||
|
||||
if (isset($latestversion->max)) {
|
||||
return ($version === $latestversion->max) ? true : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads question version ids for current question.
|
||||
*
|
||||
* @param string $questionbankentryid Question bank entry id
|
||||
* @return array $questionids Array containing question id as key and version as value.
|
||||
*/
|
||||
public static function load_versions(string $questionbankentryid) : array {
|
||||
global $DB;
|
||||
|
||||
$questionids = [];
|
||||
$sql = 'SELECT version, questionid
|
||||
FROM {question_versions}
|
||||
WHERE questionbankentryid = ?';
|
||||
|
||||
$versions = $DB->get_records_sql($sql, [$questionbankentryid]);
|
||||
foreach ($versions as $key => $version) {
|
||||
$questionids[$version->questionid] = $key;
|
||||
}
|
||||
return $questionids;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,3 +25,14 @@
|
|||
|
||||
$string['pluginname'] = 'Preview question';
|
||||
$string['privacy:metadata'] = 'The Preview question question bank plugin does not store any personal data.';
|
||||
// Tag related errors.
|
||||
$string['tagclosebutton'] = 'Close';
|
||||
$string['tagerror'] = 'No question was found with following tags: {$a}. Please change or remove tags filtering.';
|
||||
$string['tagsnotfound'] = 'Tags not found';
|
||||
// Form string(s).
|
||||
$string['previewoptions'] = 'Preview options';
|
||||
$string['questionversion'] = 'Question version';
|
||||
// Preview title.
|
||||
$string['versiontitle'] = 'Version {$a}';
|
||||
$string['versiontitlelatest'] = 'Version {$a} (latest)';
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
use \core\notification;
|
||||
use qbank_previewquestion\form\preview_options_form;
|
||||
use qbank_previewquestion\question_preview_options;
|
||||
use qbank_previewquestion\helper;
|
||||
|
@ -48,6 +49,7 @@ define('QUESTION_PREVIEW_MAX_VARIANTS', 100);
|
|||
// Get and validate question id.
|
||||
$id = required_param('id', PARAM_INT);
|
||||
$returnurl = optional_param('returnurl', null, PARAM_RAW);
|
||||
|
||||
$question = question_bank::load_question($id);
|
||||
|
||||
if ($returnurl) {
|
||||
|
@ -132,8 +134,10 @@ $options->behaviour = $quba->get_preferred_behaviour();
|
|||
$options->maxmark = $quba->get_question_max_mark($slot);
|
||||
|
||||
// Create the settings form, and initialise the fields.
|
||||
$optionsform = new preview_options_form(helper::question_preview_form_url($question->id, $context, $previewid, $returnurl),
|
||||
['quba' => $quba, 'maxvariant' => $maxvariant]);
|
||||
$versionids = helper::load_versions($question->questionbankentryid);
|
||||
$optionsform = new preview_options_form(helper::
|
||||
question_preview_form_url($question->id, $context, $previewid, $returnurl),
|
||||
['quba' => $quba, 'maxvariant' => $maxvariant, 'versions' => $versionids, 'questionversion' => $id]);
|
||||
$optionsform->set_data($options);
|
||||
|
||||
// Process change of settings, if that was requested.
|
||||
|
@ -144,7 +148,7 @@ if ($newoptions = $optionsform->get_submitted_data()) {
|
|||
$newoptions->variant = $options->variant;
|
||||
}
|
||||
if (isset($newoptions->saverestart)) {
|
||||
helper::restart_preview($previewid, $question->id, $newoptions, $context, $returnurl);
|
||||
helper::restart_preview($previewid, $question->id, $newoptions, $context, $returnurl, $newoptions->version);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,6 +253,17 @@ $PAGE->set_heading($title);
|
|||
echo $OUTPUT->header();
|
||||
|
||||
$previewdata = [];
|
||||
|
||||
$previewdata['questionicon'] = print_question_icon($question);
|
||||
$previewdata['questionidumber'] = $question->idnumber;
|
||||
$previewdata['questiontitle'] = $question->name;
|
||||
$islatestversion = is_latest($question->version, $question->questionbankentryid);
|
||||
if ($islatestversion) {
|
||||
$previewdata['versiontitle'] = get_string('versiontitlelatest', 'qbank_previewquestion', $question->version);
|
||||
} else {
|
||||
$previewdata['versiontitle'] = get_string('versiontitle', 'qbank_previewquestion', $question->version);
|
||||
}
|
||||
|
||||
$previewdata['actionurl'] = $actionurl;
|
||||
$previewdata['session'] = sesskey();
|
||||
$previewdata['slot'] = $slot;
|
||||
|
@ -265,21 +280,6 @@ foreach ($technical as $info) {
|
|||
}
|
||||
$previewdata['techinfo'] .= print_collapsible_region_end(true);
|
||||
|
||||
// Output a link to export this single question.
|
||||
if (question_has_capability_on($question, 'view')) {
|
||||
if (class_exists('qbank_exporttoxml\\helper')) {
|
||||
if (\core\plugininfo\qbank::is_plugin_enabled('qbank_exporttoxml')) {
|
||||
$exportfunction = '\\qbank_exporttoxml\\helper::question_get_export_single_question_url';
|
||||
$previewdata['exporttoxml'] = html_writer::link($exportfunction($question),
|
||||
get_string('exportonequestion', 'question'));
|
||||
}
|
||||
} else {
|
||||
$exportfunction = 'question_get_export_single_question_url';
|
||||
$previewdata['exporttoxml'] = html_writer::link($exportfunction($question),
|
||||
get_string('exportonequestion', 'question'));
|
||||
}
|
||||
}
|
||||
|
||||
// Display the settings form.
|
||||
$previewdata['options'] = $optionsform->render();
|
||||
|
||||
|
@ -304,16 +304,12 @@ if (!is_null($returnurl)) {
|
|||
$previewdata['redirect'] = true;
|
||||
$previewdata['redirecturl'] = $returnurl;
|
||||
}
|
||||
|
||||
$closeurl = new moodle_url('/question/edit.php', ['courseid' => $COURSE->id]);
|
||||
echo $PAGE->get_renderer('qbank_previewquestion')->render_preview_page($previewdata);
|
||||
|
||||
// Log the preview of this question.
|
||||
$event = \core\event\question_viewed::create_from_question_instance($question, $context);
|
||||
$event->trigger();
|
||||
|
||||
$PAGE->requires->js_call_amd('qbank_previewquestion/preview', 'init', [$previewdata['redirect']]);
|
||||
$PAGE->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['responseform']);
|
||||
$PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_save_question_preview']);
|
||||
$PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_finish_question_preview']);
|
||||
$PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_restart_question_preview']);
|
||||
$PAGE->requires->js_call_amd('qbank_previewquestion/preview', 'init', [$previewdata['redirect'], $closeurl->__toString()]);
|
||||
echo $OUTPUT->footer();
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
* session - Moodle session
|
||||
* slot - The identifying number of the first question that was added to this usage
|
||||
* question - The html of the actual question from the engine
|
||||
* questionicon - The icon of the question type
|
||||
* questiontitle - The name of the question
|
||||
* versiontitle - The string for displaying the version
|
||||
* questionidumber - The idnumber of the question
|
||||
* restartdisabled - The attributes to enable or disable the button, same for finishdisabled and filldisabled
|
||||
* techinfo - Technical information like fraction, state, behaviour etc
|
||||
* exporttoxml - Link to export the question to xml
|
||||
|
@ -31,25 +35,39 @@
|
|||
|
||||
Example context (json):
|
||||
{
|
||||
"previewdata": [
|
||||
{
|
||||
"actionurl": "/",
|
||||
"session": "E2PwCfrnzz",
|
||||
"slot": "1",
|
||||
"question": "<div>question html</div>",
|
||||
"restartdisabled": "disabled='disabled'",
|
||||
"finishdisabled": "disabled='disabled'",
|
||||
"filldisabled": "disabled='disabled'",
|
||||
"techinfo": "<div>Behaviour being used: Deferred feedback</div>",
|
||||
"redirecturl": "/",
|
||||
"exporttoxml": "Download this question in Moodle XML format",
|
||||
"comments": "html from comments api",
|
||||
"extrapreviewelements": "<div>callback to get html from plugins need to show info in preview</div>"
|
||||
}
|
||||
]
|
||||
"actionurl": "/",
|
||||
"session": "E2PwCfrnzz",
|
||||
"slot": "1",
|
||||
"question": "<div>question html</div>",
|
||||
"questionicon": "<i class='icon fa fa-search-plus fa-fw' title='Preview question' aria-label='Preview question'></i>",
|
||||
"questiontitle": "Question title",
|
||||
"versiontitle": "Version 3 (latest)",
|
||||
"questionidumber": "qidnumber1",
|
||||
"restartdisabled": "disabled='disabled'",
|
||||
"finishdisabled": "disabled='disabled'",
|
||||
"filldisabled": "disabled='disabled'",
|
||||
"techinfo": "<div>Behaviour being used: Deferred feedback</div>",
|
||||
"redirecturl": "/",
|
||||
"exporttoxml": "Download this question in Moodle XML format",
|
||||
"comments": "html from comments api",
|
||||
"extrapreviewelements": "<div>callback to get html from plugins need to show info in preview</div>"
|
||||
}
|
||||
}}
|
||||
<form id="responseform" method="post" action="{{{actionurl}}}" enctype="multipart/form-data" autocomplete="off">
|
||||
<div class="d-flex">
|
||||
<h2 class="mt-2">{{{questionicon}}}</h2>
|
||||
<h2 class="ml-2 mt-2"> {{questiontitle}}</h2>
|
||||
<h3 class="px-2 py-1 ml-2 mt-2">
|
||||
<span class="badge bg-primary text-light">{{versiontitle}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="badge-primary h-50 px-2 mt-n2">
|
||||
<span class="accesshide">ID number</span>
|
||||
{{questionidumber}}
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<input type="hidden" name="sesskey" value="{{session}}">
|
||||
<input type="hidden" name="slots" value="{{slot}}">
|
||||
|
@ -69,21 +87,21 @@
|
|||
{{/redirect}}
|
||||
</div>
|
||||
</form>
|
||||
{{{techinfo}}}
|
||||
{{{exporttoxml}}}
|
||||
<br>
|
||||
{{#comments}}
|
||||
<div class="row">
|
||||
<div class="col-6 text-left">
|
||||
{{{options}}}
|
||||
</div>
|
||||
<div class="col-6 question-comment-view">
|
||||
{{{comments}}}
|
||||
</div>
|
||||
<a data-toggle="collapse" href="#commentcollapse" role="button" aria-expanded="false" aria-controls="commentcollapse">
|
||||
{{#pix}} t/collapsed, core {{/pix}}
|
||||
{{#str}} commentplural, qbank_comment{{/str}}
|
||||
</a>
|
||||
<div class="collapse" id="commentcollapse">
|
||||
{{{comments}}}
|
||||
</div>
|
||||
{{{options}}}
|
||||
{{/comments}}
|
||||
{{^comments}}
|
||||
{{{options}}}
|
||||
{{/comments}}
|
||||
{{{techinfo}}}
|
||||
{{#extrapreviewelements}}
|
||||
{{{extrapreviewelements}}}
|
||||
{{/extrapreviewelements}}
|
||||
|
|
|
@ -30,10 +30,19 @@ Feature: A teacher can preview questions in the question bank
|
|||
|
||||
Scenario: Question preview shows the question and other information
|
||||
Then the state of "What is pi to two d.p.?" question is shown as "Not yet answered"
|
||||
And I should see "(latest)"
|
||||
And I should see "Marked out of 1.00"
|
||||
And I should see "Technical information"
|
||||
And I should see "Attempt options"
|
||||
And I should see "Display options"
|
||||
And I should see "Preview options"
|
||||
And I should see "Comments"
|
||||
And I click on "Comments" "link"
|
||||
And I should see "Save comment"
|
||||
And I should see "ID number"
|
||||
And "Numerical" "icon" should exist
|
||||
And I should see "Version"
|
||||
And I click on "Preview options" "link"
|
||||
And I should see "Question version"
|
||||
|
||||
Scenario: Preview lets the teacher see what happens when an answer is saved
|
||||
When I set the field "Answer:" to "1"
|
||||
|
@ -48,14 +57,18 @@ Feature: A teacher can preview questions in the question bank
|
|||
Scenario: Preview lets the teacher see what happens with different review options
|
||||
Given I set the field "Answer:" to "3.14"
|
||||
And I press "Submit and finish"
|
||||
And I press "Display options"
|
||||
When I set the field "Whether correct" to "Not shown"
|
||||
And I set the field "Decimal places in grades" to "5"
|
||||
And I press "Update display options"
|
||||
And I set the field "Answer:" to "3.14"
|
||||
And I press "Submit and finish"
|
||||
Then the state of "What is pi to two d.p.?" question is shown as "Complete"
|
||||
And I should see "1.00000"
|
||||
|
||||
Scenario: Preview lets the teacher see what happens with different behaviours
|
||||
When I set the field "How questions behave" to "Immediate feedback"
|
||||
When I press "Preview options"
|
||||
And I set the field "How questions behave" to "Immediate feedback"
|
||||
And I set the field "Marked out of" to "3"
|
||||
And I press "Start again with these options"
|
||||
And I set the field "Answer:" to "3.1"
|
||||
|
@ -74,10 +87,28 @@ Feature: A teacher can preview questions in the question bank
|
|||
When I press "Fill in correct responses"
|
||||
Then the field "Answer:" matches value "3.14"
|
||||
|
||||
Scenario: Preview has an option to export the individual quesiton.
|
||||
Then following "Download this question in Moodle XML format" should download between "1000" and "2500" bytes
|
||||
|
||||
Scenario: Preview a question with very small grade
|
||||
When I set the field "Marked out of" to "0.00000123456789"
|
||||
When I press "Preview options"
|
||||
And I set the field "Marked out of" to "0.00000123456789"
|
||||
And I press "Start again with these options"
|
||||
Then the field "Marked out of" matches value "0.00000123456789"
|
||||
|
||||
Scenario: Question version is updated when edited and teacher can change question version
|
||||
And I should see "Version 1"
|
||||
And I press "Close preview"
|
||||
And I choose "Edit question" action for "Test question to be previewed" in the question bank
|
||||
And I set the field "Question name" to "New version"
|
||||
And I set the field "Question text" to "New text version"
|
||||
And I click on "submitbutton" "button"
|
||||
And I choose "Preview" action for "New version" in the question bank
|
||||
Then I should see "Version 2"
|
||||
And I should see "(latest)"
|
||||
And I should see "New version"
|
||||
And I should see "New text version"
|
||||
And I should not see "Test question to be previewed"
|
||||
And I should not see "Version 1"
|
||||
|
||||
Scenario: Question preview can be closed
|
||||
And I press "Close preview"
|
||||
Then I should not see "(latest)"
|
||||
And I should see "Course 1"
|
||||
|
|
|
@ -16,7 +16,12 @@
|
|||
|
||||
namespace qbank_previewquestion;
|
||||
|
||||
use context_course;
|
||||
use moodle_url;
|
||||
use core\plugininfo\qbank;
|
||||
use question_bank;
|
||||
use question_engine;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Helper tests for question preview.
|
||||
|
@ -67,17 +72,18 @@ class helper_test extends \advanced_testcase {
|
|||
$questiongenerator = $generator->get_plugin_generator('core_question');
|
||||
// Create a course.
|
||||
$course = $generator->create_course();
|
||||
$this->context = \context_course::instance($course->id);
|
||||
$this->context = context_course::instance($course->id);
|
||||
// Create a question in the default category.
|
||||
$contexts = new \question_edit_contexts($this->context);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($this->context);
|
||||
$cat = question_make_default_categories($contexts->all());
|
||||
$this->questiondata = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $cat->id]);
|
||||
$this->quba = \question_engine::make_questions_usage_by_activity('core_question_preview', \context_user::instance($USER->id));
|
||||
$this->quba = question_engine::make_questions_usage_by_activity('core_question_preview',
|
||||
\context_user::instance($USER->id));
|
||||
$this->options = new question_preview_options($this->questiondata);
|
||||
$this->options->load_user_defaults();
|
||||
$this->options->set_from_request();
|
||||
$this->returnurl = new \moodle_url('/question/edit.php');
|
||||
$this->returnurl = new moodle_url('/question/edit.php');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,7 +101,7 @@ class helper_test extends \advanced_testcase {
|
|||
'courseid' => $this->context->instanceid
|
||||
];
|
||||
$params = array_merge($params, $this->options->get_url_params());
|
||||
$expectedurl = new \moodle_url('/question/bank/previewquestion/preview.php', $params);
|
||||
$expectedurl = new moodle_url('/question/bank/previewquestion/preview.php', $params);
|
||||
$this->assertEquals($expectedurl, $actionurl);
|
||||
}
|
||||
|
||||
|
@ -112,7 +118,7 @@ class helper_test extends \advanced_testcase {
|
|||
'returnurl' => $this->returnurl,
|
||||
'courseid' => $this->context->instanceid
|
||||
];
|
||||
$expectedurl = new \moodle_url('/question/bank/previewquestion/preview.php', $params);
|
||||
$expectedurl = new moodle_url('/question/bank/previewquestion/preview.php', $params);
|
||||
$this->assertEquals($expectedurl, $formurl);
|
||||
}
|
||||
|
||||
|
@ -138,7 +144,7 @@ class helper_test extends \advanced_testcase {
|
|||
$params['generalfeedback'] = (bool) $this->options->generalfeedback;
|
||||
$params['rightanswer'] = (bool) $this->options->rightanswer;
|
||||
$params['history'] = (bool) $this->options->history;
|
||||
$expectedurl = new \moodle_url('/question/bank/previewquestion/preview.php', $params);
|
||||
$expectedurl = new moodle_url('/question/bank/previewquestion/preview.php', $params);
|
||||
$this->assertEquals($expectedurl, $previewurl);
|
||||
}
|
||||
|
||||
|
@ -159,4 +165,27 @@ class helper_test extends \advanced_testcase {
|
|||
$this->assertEquals('', $comment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method load_versions().
|
||||
*
|
||||
* @covers ::load_versions
|
||||
*/
|
||||
public function test_load_versions() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$qcat1 = $generator->create_question_category(['name' => 'My category', 'sortorder' => 1, 'idnumber' => 'myqcat']);
|
||||
$questiongenerated = $generator->create_question('description', null, ['name' => 'q1', 'category' => $qcat1->id]);
|
||||
$qtypeobj = question_bank::get_qtype($questiongenerated->qtype);
|
||||
$question = question_bank::load_question($questiongenerated->id);
|
||||
$versionids = helper::load_versions($question->questionbankentryid);
|
||||
$this->assertCount(1, $versionids);
|
||||
$fromform = new stdClass();
|
||||
$fromform->name = 'Name edited';
|
||||
$fromform->category = $qcat1->id;
|
||||
$qtypeobj->save_question($questiongenerated, $fromform);
|
||||
$versionids = helper::load_versions($question->questionbankentryid);
|
||||
$this->assertCount(2, $versionids);
|
||||
}
|
||||
}
|
|
@ -79,15 +79,17 @@ class submit_tags extends external_api {
|
|||
|
||||
if (!$question = $DB->get_record_sql('
|
||||
SELECT q.*, qc.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON qc.id = q.category
|
||||
WHERE q.id = ?', [$params['questionid']])) {
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.id = ?', [$questionid])) {
|
||||
throw new \moodle_exception('questiondoesnotexist', 'question');
|
||||
}
|
||||
|
||||
$cantag = question_has_capability_on($question, 'tag');
|
||||
$questioncontext = \context::instance_by_id($question->contextid);
|
||||
$contexts = new \question_edit_contexts($editingcontext);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($editingcontext);
|
||||
|
||||
$formoptions = [
|
||||
'editingcontext' => $editingcontext,
|
||||
|
|
|
@ -53,9 +53,15 @@ function qbank_tagquestion_output_fragment_tags_form($args) {
|
|||
$filtercourses = null;
|
||||
}
|
||||
|
||||
$category = $DB->get_record('question_categories', ['id' => $question->category]);
|
||||
$sql = "SELECT qc.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.id = :id";
|
||||
$category = $DB->get_record_sql($sql, ['id' => $question->id]);
|
||||
$questioncontext = \context::instance_by_id($category->contextid);
|
||||
$contexts = new \question_edit_contexts($editingcontext);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($editingcontext);
|
||||
|
||||
// Load the question tags and filter the course tags by the current course.
|
||||
if (core_tag_tag::is_enabled('core_question', 'question')) {
|
||||
|
|
|
@ -37,7 +37,7 @@ class helper {
|
|||
|
||||
$sql = 'SELECT COUNT(*) FROM (' . self::question_usage_sql() . ') quizid';
|
||||
|
||||
return $DB->count_records_sql($sql, [$question->id, $question->id]);
|
||||
return $DB->count_records_sql($sql, [$question->id, $question->questionbankentryid]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,11 +58,14 @@ class helper {
|
|||
AND q.id = ?)
|
||||
UNION
|
||||
(SELECT qz.id as quizid,
|
||||
qz.name as modulename,
|
||||
qz.course as courseid
|
||||
qz.name as modulename,
|
||||
qz.course as courseid
|
||||
FROM {quiz_slots} slot
|
||||
JOIN {quiz} qz ON qz.id = slot.quizid
|
||||
WHERE slot.questionid = ?)";
|
||||
JOIN {question_references} qr ON qr.itemid = slot.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid
|
||||
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
||||
WHERE qv.questionbankentryid = ?)";
|
||||
return $sqlset;
|
||||
}
|
||||
|
||||
|
@ -73,18 +76,31 @@ class helper {
|
|||
* @param int $quizid
|
||||
* @return int
|
||||
*/
|
||||
public static function get_question_attempts_count_in_quiz(int $questionid, int $quizid): int {
|
||||
public static function get_question_attempts_count_in_quiz(int $questionid, $quizid = null): int {
|
||||
global $DB;
|
||||
$sql = 'SELECT COUNT(qatt.id)
|
||||
FROM {quiz} qz
|
||||
JOIN {quiz_attempts} qa ON qa.quiz = qz.id
|
||||
JOIN {question_usages} qu ON qu.id = qa.uniqueid
|
||||
JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id
|
||||
JOIN {question} q ON q.id = qatt.questionid
|
||||
WHERE qatt.questionid = :questionid
|
||||
AND qa.preview = 0
|
||||
AND qz.id = :quizid';
|
||||
return $DB->count_records_sql($sql, [ 'questionid' => $questionid, 'quizid' => $quizid]);
|
||||
if ($quizid) {
|
||||
$sql = 'SELECT COUNT(qatt.id)
|
||||
FROM {quiz} qz
|
||||
JOIN {quiz_attempts} qa ON qa.quiz = qz.id
|
||||
JOIN {question_usages} qu ON qu.id = qa.uniqueid
|
||||
JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id
|
||||
JOIN {question} q ON q.id = qatt.questionid
|
||||
WHERE qatt.questionid = :questionid
|
||||
AND qa.preview = 0
|
||||
AND qz.id = :quizid';
|
||||
$param = ['questionid' => $questionid, 'quizid' => $quizid];
|
||||
} else {
|
||||
$sql = 'SELECT COUNT(qatt.id)
|
||||
FROM {quiz_slots} qs
|
||||
JOIN {quiz_attempts} qa ON qa.quiz = qs.quizid
|
||||
JOIN {question_usages} qu ON qu.id = qa.uniqueid
|
||||
JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id
|
||||
JOIN {question} q ON q.id = qatt.questionid
|
||||
WHERE qatt.questionid = ?
|
||||
AND qa.preview = 0';
|
||||
$param = ['questionid' => $questionid];
|
||||
}
|
||||
return $DB->count_records_sql($sql, $param);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ class question_usage_table extends table_sql {
|
|||
}
|
||||
|
||||
$sql = helper::question_usage_sql();
|
||||
$params = [$this->question->id, $this->question->id];
|
||||
$params = [$this->question->id, $this->question->questionbankentryid];
|
||||
|
||||
if (!$this->is_downloading()) {
|
||||
$this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size());
|
||||
|
|
|
@ -98,7 +98,7 @@ class helper_test extends \advanced_testcase {
|
|||
*/
|
||||
public function test_get_question_entry_usage_count() {
|
||||
foreach ($this->questions as $question) {
|
||||
$count = helper::get_question_entry_usage_count($question);
|
||||
$count = helper::get_question_entry_usage_count(\question_bank::load_question($question->id));
|
||||
// Test that the attempt data matches the usage data for the count.
|
||||
$this->assertEquals(1, $count);
|
||||
}
|
||||
|
|
|
@ -1,74 +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_viewcreator;
|
||||
|
||||
use core_question\local\bank\column_base;
|
||||
|
||||
/**
|
||||
* A column type for the name of the question last modifier.
|
||||
*
|
||||
* @package qbank_viewcreator
|
||||
* @copyright 2009 Tim Hunt
|
||||
* @author 2021 Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class modifier_name_column extends column_base {
|
||||
|
||||
public function get_name(): string {
|
||||
return 'modifiername';
|
||||
}
|
||||
|
||||
protected function get_title(): string {
|
||||
return get_string('lastmodifiedby', 'question');
|
||||
}
|
||||
|
||||
protected function display_content($question, $rowclasses): void {
|
||||
global $PAGE;
|
||||
$displaydata = [];
|
||||
|
||||
if (!empty($question->modifierfirstname) && !empty($question->modifierlastname)) {
|
||||
$u = new \stdClass();
|
||||
$u = username_load_fields_from_object($u, $question, 'modifier');
|
||||
$displaydata['date'] = userdate($question->timemodified, get_string('strftimedatetime', 'langconfig'));
|
||||
$displaydata['modifier'] = fullname($u);
|
||||
echo $PAGE->get_renderer('qbank_viewcreator')->render_modifier_name($displaydata);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_extra_joins(): array {
|
||||
return ['um' => 'LEFT JOIN {user} um ON um.id = q.modifiedby'];
|
||||
}
|
||||
|
||||
public function get_required_fields(): array {
|
||||
$allnames = \core_user\fields::get_name_fields();
|
||||
$requiredfields = [];
|
||||
foreach ($allnames as $allname) {
|
||||
$requiredfields[] = 'um.' . $allname . ' AS modifier' . $allname;
|
||||
}
|
||||
$requiredfields[] = 'q.timemodified';
|
||||
return $requiredfields;
|
||||
}
|
||||
|
||||
public function is_sortable(): array {
|
||||
return [
|
||||
'firstname' => ['field' => 'um.firstname', 'title' => get_string('firstname')],
|
||||
'lastname' => ['field' => 'um.lastname', 'title' => get_string('lastname')],
|
||||
'timemodified' => ['field' => 'q.timemodified', 'title' => get_string('date')]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -36,13 +36,13 @@ class renderer extends \plugin_renderer_base {
|
|||
}
|
||||
|
||||
/**
|
||||
* Render question modifier.
|
||||
* Render question edit form callback.
|
||||
*
|
||||
* @param array $displaydata
|
||||
* @return string
|
||||
*/
|
||||
public function render_modifier_name($displaydata) {
|
||||
return $this->render_from_template('qbank_viewcreator/modifier_display', $displaydata);
|
||||
public function render_version_info($displaydata) {
|
||||
return $this->render_from_template('qbank_viewcreator/version_info', $displaydata);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,8 +30,7 @@ class plugin_feature extends plugin_features_base {
|
|||
|
||||
public function get_question_columns($qbank): array {
|
||||
return [
|
||||
new creator_name_column($qbank),
|
||||
new modifier_name_column($qbank)
|
||||
new creator_name_column($qbank)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,5 @@
|
|||
*/
|
||||
|
||||
$string['pluginname'] = 'View creator';
|
||||
$string['privacy:metadata'] = 'The View creator question bank plugin does not store any personal data.';
|
||||
$string['privacy:metadata'] = 'View creator question bank plugin does not store any user data.';
|
||||
$string['version'] = 'Version {$a}';
|
||||
|
|
46
question/bank/viewcreator/lib.php
Normal file
46
question/bank/viewcreator/lib.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?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_viewcreator
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edit page callback for information.
|
||||
*
|
||||
* @param object $question
|
||||
* @return string
|
||||
*/
|
||||
function qbank_viewcreator_edit_form_display($question): string {
|
||||
global $DB, $PAGE;
|
||||
$versiondata = [];
|
||||
$questionversion = $DB->get_record('question_versions', ['questionid' => $question->id])->version;
|
||||
$versiondata['versionnumber'] = $questionversion;
|
||||
if (!empty($question->createdby)) {
|
||||
$a = new stdClass();
|
||||
$a->time = userdate($question->timecreated);
|
||||
$a->user = fullname($DB->get_record('user', ['id' => $question->createdby]));
|
||||
$versiondata['createdby'] = get_string('created', 'question') . ' ' .
|
||||
get_string('byandon', 'question', $a);
|
||||
}
|
||||
return $PAGE->get_renderer('qbank_viewcreator')->render_version_info($versiondata);
|
||||
|
||||
}
|
37
question/bank/viewcreator/templates/version_info.mustache
Normal file
37
question/bank/viewcreator/templates/version_info.mustache
Normal file
|
@ -0,0 +1,37 @@
|
|||
{{!
|
||||
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/version_info
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"addquestiondata": [
|
||||
{
|
||||
"versionnumber": 1,
|
||||
"createdby": "Admin User on Wednesday, 20 October 2021, 5:33 AM"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="question-version-number">
|
||||
<a><u>{{#str}} version, qbank_viewcreator, {{versionnumber}} {{/str}}</u></a>
|
||||
</div>
|
||||
{{#createdby}}
|
||||
<div class="question-creator-info">
|
||||
<a>{{{createdby}}}</a>
|
||||
</div>
|
||||
{{/createdby}}
|
|
@ -24,10 +24,8 @@ Feature: Use the qbank plugin manager page for viewcreator plugin
|
|||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank" in current page administration
|
||||
Then I should not see "Created by"
|
||||
And I should not see "Last modified by"
|
||||
And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration
|
||||
And I click on "Enable" "link" in the "View creator" "table_row"
|
||||
And I am on the "Test quiz" "quiz activity" page
|
||||
And I navigate to "Question bank" in current page administration
|
||||
Then I should see "Created by"
|
||||
And I should see "Last modified by"
|
||||
|
|
|
@ -64,14 +64,14 @@ class question_name_idnumber_tags_column extends viewquestionname_column_helper
|
|||
|
||||
public function get_required_fields(): array {
|
||||
$fields = parent::get_required_fields();
|
||||
$fields[] = 'q.idnumber';
|
||||
$fields[] = 'qbe.idnumber';
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function is_sortable(): array {
|
||||
return [
|
||||
'name' => ['field' => 'q.name', 'title' => get_string('questionname', 'question')],
|
||||
'idnumber' => ['field' => 'q.idnumber', 'title' => get_string('idnumber', 'question')],
|
||||
'idnumber' => ['field' => 'qbe.idnumber', 'title' => get_string('idnumber', 'question')],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -60,12 +60,8 @@ class question_text_row extends row_base {
|
|||
echo $text;
|
||||
}
|
||||
|
||||
public function get_extra_joins(): array {
|
||||
return ['qc' => 'JOIN {question_categories} qc ON qc.id = q.category'];
|
||||
}
|
||||
|
||||
public function get_required_fields(): array {
|
||||
return ['q.id', 'q.questiontext', 'q.questiontextformat', 'qc.contextid'];
|
||||
return ['q.questiontext', 'q.questiontextformat'];
|
||||
}
|
||||
|
||||
public function has_preference(): bool {
|
||||
|
|
|
@ -92,7 +92,7 @@ class category_condition extends condition {
|
|||
$categoryids = [$this->category->id];
|
||||
}
|
||||
list($catidtest, $this->params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cat');
|
||||
$this->where = 'q.category ' . $catidtest;
|
||||
$this->where = 'qbe.questioncategoryid ' . $catidtest;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
namespace core_question\bank\search;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
|
||||
/**
|
||||
* This class controls whether hidden / deleted questions are hidden in the list.
|
||||
*
|
||||
|
@ -46,7 +48,8 @@ class hidden_condition extends condition {
|
|||
public function __construct($hide = true) {
|
||||
$this->hide = $hide;
|
||||
if ($hide) {
|
||||
$this->where = 'q.hidden = 0';
|
||||
$this->where = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' " .
|
||||
" OR qv.status = '" . question_version_status::QUESTION_STATUS_DRAFT . "' ";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ class core_question_external extends external_api {
|
|||
|
||||
$cantag = question_has_capability_on($question, 'tag');
|
||||
$questioncontext = \context::instance_by_id($question->contextid);
|
||||
$contexts = new \question_edit_contexts($editingcontext);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($editingcontext);
|
||||
|
||||
$formoptions = [
|
||||
'editingcontext' => $editingcontext,
|
||||
|
@ -300,7 +300,7 @@ class core_question_external extends external_api {
|
|||
|
||||
$categorycontextid = $DB->get_field('question_categories', 'contextid', ['id' => $categoryid], MUST_EXIST);
|
||||
$categorycontext = \context::instance_by_id($categorycontextid);
|
||||
$editcontexts = new \question_edit_contexts($categorycontext);
|
||||
$editcontexts = new \core_question\local\bank\question_edit_contexts($categorycontext);
|
||||
// The user must be able to view all questions in the category that they are requesting.
|
||||
$editcontexts->require_cap('moodle/question:viewall');
|
||||
|
||||
|
|
|
@ -47,7 +47,9 @@ abstract class action_column_base extends column_base {
|
|||
}
|
||||
|
||||
public function get_extra_joins(): array {
|
||||
return ['qc' => 'JOIN {question_categories} qc ON qc.id = q.category'];
|
||||
return ['qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id',
|
||||
'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid',
|
||||
'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid'];
|
||||
}
|
||||
|
||||
public function get_required_fields(): array {
|
||||
|
|
92
question/classes/local/bank/context_to_string_translator.php
Normal file
92
question/classes/local/bank/context_to_string_translator.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?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 core_question\local\bank;
|
||||
|
||||
/**
|
||||
* Converts contextlevels to strings and back to help with reading/writing contexts to/from import/export files.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class context_to_string_translator {
|
||||
|
||||
/**
|
||||
* @var array used to translate between contextids and strings for this context.
|
||||
*/
|
||||
protected $contexttostringarray = [];
|
||||
|
||||
/**
|
||||
* context_to_string_translator constructor.
|
||||
*
|
||||
* @param \context[] $contexts
|
||||
*/
|
||||
public function __construct($contexts) {
|
||||
$this->generate_context_to_string_array($contexts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Context to string.
|
||||
*
|
||||
* @param int $contextid
|
||||
* @return mixed
|
||||
*/
|
||||
public function context_to_string($contextid) {
|
||||
return $this->contexttostringarray[$contextid];
|
||||
}
|
||||
|
||||
/**
|
||||
* String to context.
|
||||
*
|
||||
* @param string $contextname
|
||||
* @return false|int|string
|
||||
*/
|
||||
public function string_to_context($contextname) {
|
||||
return array_search($contextname, $this->contexttostringarray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate context to array.
|
||||
*
|
||||
* @param \context[] $contexts
|
||||
*/
|
||||
protected function generate_context_to_string_array($contexts) {
|
||||
if (!$this->contexttostringarray) {
|
||||
$catno = 1;
|
||||
foreach ($contexts as $context) {
|
||||
switch ($context->contextlevel) {
|
||||
case CONTEXT_MODULE :
|
||||
$contextstring = 'module';
|
||||
break;
|
||||
case CONTEXT_COURSE :
|
||||
$contextstring = 'course';
|
||||
break;
|
||||
case CONTEXT_COURSECAT :
|
||||
$contextstring = "cat$catno";
|
||||
$catno++;
|
||||
break;
|
||||
case CONTEXT_SYSTEM :
|
||||
$contextstring = 'system';
|
||||
break;
|
||||
}
|
||||
$this->contexttostringarray[$context->id] = $contextstring;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -97,4 +97,12 @@ class edit_menu_column extends column_base {
|
|||
return ['q.qtype'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get menuable actions.
|
||||
*
|
||||
* @return menuable_action Menuable actions.
|
||||
*/
|
||||
public function get_actions(): array {
|
||||
return $this->actions;
|
||||
}
|
||||
}
|
||||
|
|
222
question/classes/local/bank/question_edit_contexts.php
Normal file
222
question/classes/local/bank/question_edit_contexts.php
Normal file
|
@ -0,0 +1,222 @@
|
|||
<?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 core_question\local\bank;
|
||||
|
||||
/**
|
||||
* Tracks all the contexts related to the one we are currently editing questions and provides helper methods to check permissions.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2007 Jamie Pratt me@jamiep.org
|
||||
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_edit_contexts {
|
||||
|
||||
/**
|
||||
* @var \string[][] array of the capabilities.
|
||||
*/
|
||||
public static $caps = [
|
||||
'editq' => [
|
||||
'moodle/question:add',
|
||||
'moodle/question:editmine',
|
||||
'moodle/question:editall',
|
||||
'moodle/question:viewmine',
|
||||
'moodle/question:viewall',
|
||||
'moodle/question:usemine',
|
||||
'moodle/question:useall',
|
||||
'moodle/question:movemine',
|
||||
'moodle/question:moveall'],
|
||||
'questions' => [
|
||||
'moodle/question:add',
|
||||
'moodle/question:editmine',
|
||||
'moodle/question:editall',
|
||||
'moodle/question:viewmine',
|
||||
'moodle/question:viewall',
|
||||
'moodle/question:movemine',
|
||||
'moodle/question:moveall'],
|
||||
'categories' => [
|
||||
'moodle/question:managecategory'],
|
||||
'import' => [
|
||||
'moodle/question:add'],
|
||||
'export' => [
|
||||
'moodle/question:viewall',
|
||||
'moodle/question:viewmine']];
|
||||
|
||||
/**
|
||||
* @var array of contexts.
|
||||
*/
|
||||
protected $allcontexts;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param \context $thiscontext the current context.
|
||||
*/
|
||||
public function __construct(\context $thiscontext) {
|
||||
$this->allcontexts = array_values($thiscontext->get_parent_contexts(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the contexts.
|
||||
*
|
||||
* @return \context[] all parent contexts
|
||||
*/
|
||||
public function all() {
|
||||
return $this->allcontexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lowest context.
|
||||
*
|
||||
* @return \context lowest context which must be either the module or course context
|
||||
*/
|
||||
public function lowest() {
|
||||
return $this->allcontexts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contexts having cap.
|
||||
*
|
||||
* @param string $cap capability
|
||||
* @return \context[] parent contexts having capability, zero based index
|
||||
*/
|
||||
public function having_cap($cap) {
|
||||
$contextswithcap = [];
|
||||
foreach ($this->allcontexts as $context) {
|
||||
if (has_capability($cap, $context)) {
|
||||
$contextswithcap[] = $context;
|
||||
}
|
||||
}
|
||||
return $contextswithcap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contexts having at least one cap.
|
||||
*
|
||||
* @param array $caps capabilities
|
||||
* @return \context[] parent contexts having at least one of $caps, zero based index
|
||||
*/
|
||||
public function having_one_cap($caps) {
|
||||
$contextswithacap = [];
|
||||
foreach ($this->allcontexts as $context) {
|
||||
foreach ($caps as $cap) {
|
||||
if (has_capability($cap, $context)) {
|
||||
$contextswithacap[] = $context;
|
||||
break; // Done with caps loop.
|
||||
}
|
||||
}
|
||||
}
|
||||
return $contextswithacap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context having at least one cap.
|
||||
*
|
||||
* @param string $tabname edit tab name
|
||||
* @return \context[] parent contexts having at least one of $caps, zero based index
|
||||
*/
|
||||
public function having_one_edit_tab_cap($tabname) {
|
||||
return $this->having_one_cap(self::$caps[$tabname]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contexts for adding question and also using it.
|
||||
*
|
||||
* @return \context[] those contexts where a user can add a question and then use it.
|
||||
*/
|
||||
public function having_add_and_use() {
|
||||
$contextswithcap = [];
|
||||
foreach ($this->allcontexts as $context) {
|
||||
if (!has_capability('moodle/question:add', $context)) {
|
||||
continue;
|
||||
}
|
||||
if (!has_any_capability(['moodle/question:useall', 'moodle/question:usemine'], $context)) {
|
||||
continue;
|
||||
}
|
||||
$contextswithcap[] = $context;
|
||||
}
|
||||
return $contextswithcap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has at least one parent context got the cap $cap?
|
||||
*
|
||||
* @param string $cap capability
|
||||
* @return boolean
|
||||
*/
|
||||
public function have_cap($cap) {
|
||||
return (count($this->having_cap($cap)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Has at least one parent context got one of the caps $caps?
|
||||
*
|
||||
* @param array $caps capability
|
||||
* @return boolean
|
||||
*/
|
||||
public function have_one_cap($caps) {
|
||||
foreach ($caps as $cap) {
|
||||
if ($this->have_cap($cap)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has at least one parent context got one of the caps for actions on $tabname
|
||||
*
|
||||
* @param string $tabname edit tab name
|
||||
* @return boolean
|
||||
*/
|
||||
public function have_one_edit_tab_cap($tabname) {
|
||||
return $this->have_one_cap(self::$caps[$tabname]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw error if at least one parent context hasn't got the cap $cap
|
||||
*
|
||||
* @param string $cap capability
|
||||
*/
|
||||
public function require_cap($cap) {
|
||||
if (!$this->have_cap($cap)) {
|
||||
throw new \moodle_exception('nopermissions', '', '', $cap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw error if at least one parent context hasn't got one of the caps $caps
|
||||
*
|
||||
* @param array $caps capabilities
|
||||
*/
|
||||
public function require_one_cap($caps) {
|
||||
if (!$this->have_one_cap($caps)) {
|
||||
$capsstring = join(', ', $caps);
|
||||
throw new \moodle_exception('nopermissions', '', '', $capsstring);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw error if at least one parent context hasn't got one of the caps $caps
|
||||
*
|
||||
* @param string $tabname edit tab name
|
||||
*/
|
||||
public function require_one_edit_tab_cap($tabname) {
|
||||
if (!$this->have_one_edit_tab_cap($tabname)) {
|
||||
throw new \moodle_exception('nopermissions', '', '', 'access question edit tab '.$tabname);
|
||||
}
|
||||
}
|
||||
}
|
43
question/classes/local/bank/question_version_status.php
Normal file
43
question/classes/local/bank/question_version_status.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?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 core_question\local\bank;
|
||||
|
||||
/**
|
||||
* Class question_version_status contains the statuses for a question.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_version_status {
|
||||
|
||||
/**
|
||||
* Const if the question is ready to use.
|
||||
*/
|
||||
const QUESTION_STATUS_READY = 'ready';
|
||||
|
||||
/**
|
||||
* Const if the question is hidden.
|
||||
*/
|
||||
const QUESTION_STATUS_HIDDEN = 'hidden';
|
||||
|
||||
/**
|
||||
* const if the question is in draft.
|
||||
*/
|
||||
const QUESTION_STATUS_DRAFT = 'draft';
|
||||
}
|
|
@ -290,7 +290,25 @@ class random_question_loader {
|
|||
$fieldsstring = implode(',', $fields);
|
||||
}
|
||||
|
||||
return $DB->get_records_list('question', 'id', $questionids, 'id', $fieldsstring, $offset, $limit);
|
||||
// Create the query to get the questions (validate that at least we have a question id. If not, do not execute the sql).
|
||||
$hasquestions = false;
|
||||
if (!empty($questionids)) {
|
||||
$hasquestions = true;
|
||||
}
|
||||
if ($hasquestions) {
|
||||
list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid');
|
||||
$condition = 'WHERE q.id ' . $condition;
|
||||
$sql = "SELECT {$fieldsstring}
|
||||
FROM (SELECT q.*, qbe.questioncategoryid as category
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
{$condition}) q";
|
||||
|
||||
return $DB->get_records_sql($sql, $param, $offset, $limit);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -70,7 +70,7 @@ class view {
|
|||
protected $editquestionurl;
|
||||
|
||||
/**
|
||||
* @var \question_edit_contexts
|
||||
* @var \core_question\local\bank\question_edit_contexts
|
||||
*/
|
||||
protected $contexts;
|
||||
|
||||
|
@ -157,7 +157,7 @@ class view {
|
|||
/**
|
||||
* Constructor for view.
|
||||
*
|
||||
* @param \question_edit_contexts $contexts
|
||||
* @param \core_question\local\bank\question_edit_contexts $contexts
|
||||
* @param \moodle_url $pageurl
|
||||
* @param object $course course settings
|
||||
* @param object $cm (optional) activity settings.
|
||||
|
@ -244,8 +244,9 @@ class view {
|
|||
'preview_action_column',
|
||||
'delete_action_column',
|
||||
'export_xml_action_column',
|
||||
'question_status_column',
|
||||
'creator_name_column',
|
||||
'modifier_name_column'
|
||||
'comment_count_column'
|
||||
];
|
||||
if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) {
|
||||
$corequestionbankcolumns[] = 'question_text_row';
|
||||
|
@ -560,7 +561,7 @@ class view {
|
|||
protected function build_query(): void {
|
||||
// Get the required tables and fields.
|
||||
$joins = [];
|
||||
$fields = ['q.hidden', 'q.category'];
|
||||
$fields = ['qv.status', 'qc.id', 'qv.version', 'qv.id as versionid', 'qbe.id as questionbankentryid'];
|
||||
if (!empty($this->requiredcolumns)) {
|
||||
foreach ($this->requiredcolumns as $column) {
|
||||
$extrajoins = $column->get_extra_joins();
|
||||
|
@ -583,7 +584,12 @@ class view {
|
|||
}
|
||||
|
||||
// Build the where clause.
|
||||
$tests = ['q.parent = 0'];
|
||||
$latestversion = 'qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be
|
||||
ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qbe.id)';
|
||||
$tests = ['q.parent = 0', $latestversion];
|
||||
$this->sqlparams = [];
|
||||
foreach ($this->searchconditions as $searchcondition) {
|
||||
if ($searchcondition->where()) {
|
||||
|
@ -1153,7 +1159,7 @@ class view {
|
|||
*/
|
||||
protected function get_row_classes($question, $rowcount): array {
|
||||
$classes = [];
|
||||
if ($question->hidden) {
|
||||
if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) {
|
||||
$classes[] = 'dimmed_text';
|
||||
}
|
||||
if ($question->id == $this->lastchangedid) {
|
||||
|
@ -1219,4 +1225,20 @@ class view {
|
|||
$this->searchconditions[] = $searchcondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets visible columns.
|
||||
* @return array $this->visiblecolumns Visible columns.
|
||||
*/
|
||||
public function get_visiblecolumns(): array {
|
||||
return $this->visiblecolumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required columns.
|
||||
*
|
||||
* @return array Required columns.
|
||||
*/
|
||||
public function get_requiredcolumns(): array {
|
||||
return $this->requiredcolumns;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,6 +126,10 @@ class provider implements
|
|||
// The 'question_statistics' table contains aggregated statistics about responses.
|
||||
// It does not contain any identifiable user data.
|
||||
|
||||
$items->add_database_table('question_bank_entries', [
|
||||
'ownerid' => 'privacy:metadata:database:question_bank_entries:ownerid',
|
||||
], 'privacy:metadata:database:question_bank_entries');
|
||||
|
||||
// The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types.
|
||||
$items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype');
|
||||
$items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat');
|
||||
|
@ -336,12 +340,13 @@ class provider implements
|
|||
|
||||
// A user may have created or updated a question.
|
||||
// Questions are linked against a question category, which has a contextid field.
|
||||
$sql = "SELECT cat.contextid
|
||||
$sql = "SELECT qc.contextid
|
||||
FROM {question} q
|
||||
INNER JOIN {question_categories} cat ON cat.id = q.category
|
||||
WHERE
|
||||
q.createdby = :useridcreated OR
|
||||
q.modifiedby = :useridmodified";
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.createdby = :useridcreated
|
||||
OR q.modifiedby = :useridmodified";
|
||||
$params = [
|
||||
'useridcreated' => $userid,
|
||||
'useridmodified' => $userid,
|
||||
|
@ -363,9 +368,10 @@ class provider implements
|
|||
// Questions are linked against a question category, which has a contextid field.
|
||||
$sql = "SELECT q.createdby, q.modifiedby
|
||||
FROM {question} q
|
||||
JOIN {question_categories} cat
|
||||
ON cat.id = q.category
|
||||
WHERE cat.contextid = :contextid";
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = :contextid";
|
||||
|
||||
$params = [
|
||||
'contextid' => $context->id
|
||||
|
@ -487,7 +493,8 @@ class provider implements
|
|||
/**
|
||||
* Delete all data for all users in the specified context.
|
||||
*
|
||||
* @param context $context The specific context to delete data for.
|
||||
* @param \context $context The specific context to delete data for.
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public static function delete_data_for_all_users_in_context(\context $context) {
|
||||
global $DB;
|
||||
|
@ -496,17 +503,19 @@ class provider implements
|
|||
// user. They are still exported in the list of a users data, but they are not removed.
|
||||
// The userid is instead anonymised.
|
||||
|
||||
$DB->set_field_select('question', 'createdby', 0,
|
||||
'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)',
|
||||
[
|
||||
'contextid' => $context->id,
|
||||
]);
|
||||
$sql = 'SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?';
|
||||
|
||||
$DB->set_field_select('question', 'modifiedby', 0,
|
||||
'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)',
|
||||
[
|
||||
'contextid' => $context->id,
|
||||
]);
|
||||
$questions = $DB->get_records_sql($sql, [$context->id]);
|
||||
foreach ($questions as $question) {
|
||||
$question->createdby = 0;
|
||||
$question->modifiedby = 0;
|
||||
$DB->update_record('question', $question);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -523,15 +532,36 @@ class provider implements
|
|||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
$contextparams['createdby'] = $contextlist->get_user()->id;
|
||||
$DB->set_field_select('question', 'createdby', 0, "
|
||||
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
|
||||
AND createdby = :createdby", $contextparams);
|
||||
$questiondata = $DB->get_records_sql(
|
||||
"SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid {$contextsql}
|
||||
AND q.createdby = :createdby", $contextparams);
|
||||
|
||||
foreach ($questiondata as $question) {
|
||||
$question->createdby = 0;
|
||||
$DB->update_record('question', $question);
|
||||
}
|
||||
|
||||
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
|
||||
$contextparams['modifiedby'] = $contextlist->get_user()->id;
|
||||
$DB->set_field_select('question', 'modifiedby', 0, "
|
||||
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
|
||||
AND modifiedby = :modifiedby", $contextparams);
|
||||
$questiondata = $DB->get_records_sql(
|
||||
"SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid {$contextsql}
|
||||
AND q.modifiedby = :modifiedby", $contextparams);
|
||||
|
||||
foreach ($questiondata as $question) {
|
||||
$question->modifiedby = 0;
|
||||
$DB->update_record('question', $question);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -554,12 +584,32 @@ class provider implements
|
|||
|
||||
$params = ['contextid' => $context->id];
|
||||
|
||||
$DB->set_field_select('question', 'createdby', 0, "
|
||||
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
|
||||
AND createdby {$createdbysql}", $params + $createdbyparams);
|
||||
$questiondata = $DB->get_records_sql(
|
||||
"SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = :contextid
|
||||
AND q.createdby {$createdbysql}", $params + $createdbyparams);
|
||||
|
||||
$DB->set_field_select('question', 'modifiedby', 0, "
|
||||
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
|
||||
AND modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
|
||||
foreach ($questiondata as $question) {
|
||||
$question->createdby = 0;
|
||||
$DB->update_record('question', $question);
|
||||
}
|
||||
|
||||
$questiondata = $DB->get_records_sql(
|
||||
"SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = :contextid
|
||||
AND q.modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
|
||||
|
||||
foreach ($questiondata as $question) {
|
||||
$question->modifiedby = 0;
|
||||
$DB->update_record('question', $question);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class all_calculated_for_qubaid_condition {
|
|||
/**
|
||||
* @var object[]
|
||||
*/
|
||||
public $subquestions;
|
||||
public $subquestions = [];
|
||||
|
||||
/**
|
||||
* Holds slot (position) stats and stats for variants of questions in slots.
|
||||
|
|
|
@ -346,7 +346,15 @@ class calculator {
|
|||
* @param calculated $stats question stats to update.
|
||||
*/
|
||||
protected function initial_question_walker($stats) {
|
||||
$stats->markaverage = $stats->totalmarks / $stats->s;
|
||||
if ($stats->s != 0) {
|
||||
$stats->markaverage = $stats->totalmarks / $stats->s;
|
||||
$stats->othermarkaverage = $stats->totalothermarks / $stats->s;
|
||||
$stats->summarksaverage = $stats->totalsummarks / $stats->s;
|
||||
} else {
|
||||
$stats->markaverage = 0;
|
||||
$stats->othermarkaverage = 0;
|
||||
$stats->summarksaverage = 0;
|
||||
}
|
||||
|
||||
if ($stats->maxmark != 0) {
|
||||
$stats->facility = $stats->markaverage / $stats->maxmark;
|
||||
|
@ -354,10 +362,6 @@ class calculator {
|
|||
$stats->facility = null;
|
||||
}
|
||||
|
||||
$stats->othermarkaverage = $stats->totalothermarks / $stats->s;
|
||||
|
||||
$stats->summarksaverage = $stats->totalsummarks / $stats->s;
|
||||
|
||||
sort($stats->markarray, SORT_NUMERIC);
|
||||
sort($stats->othermarksarray, SORT_NUMERIC);
|
||||
|
||||
|
|
|
@ -50,40 +50,60 @@ function get_module_from_cmid($cmid) {
|
|||
|
||||
return array($modrec, $cmrec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to read all questions for category into big array
|
||||
*
|
||||
* @param int $category category number
|
||||
* @param bool $noparent if true only questions with NO parent will be selected
|
||||
* @param bool $recurse include subdirectories
|
||||
* @param bool $export set true if this is called by questionbank export
|
||||
*/
|
||||
function get_questions_category( $category, $noparent=false, $recurse=true, $export=true ) {
|
||||
* Function to read all questions for category into big array
|
||||
*
|
||||
* @param object $category category number
|
||||
* @param bool $noparent if true only questions with NO parent will be selected
|
||||
* @param bool $recurse include subdirectories
|
||||
* @param bool $export set true if this is called by questionbank export
|
||||
* @param bool $latestversion if only the latest versions needed
|
||||
* @return array
|
||||
*/
|
||||
function get_questions_category(object $category, bool $noparent, bool $recurse = true, bool $export = true,
|
||||
bool $latestversion = false): array {
|
||||
global $DB;
|
||||
|
||||
// Build sql bit for $noparent
|
||||
// Build sql bit for $noparent.
|
||||
$npsql = '';
|
||||
if ($noparent) {
|
||||
$npsql = " and parent='0' ";
|
||||
$npsql = " and q.parent='0' ";
|
||||
}
|
||||
|
||||
// Get list of categories
|
||||
// Get list of categories.
|
||||
if ($recurse) {
|
||||
$categorylist = question_categorylist($category->id);
|
||||
} else {
|
||||
$categorylist = array($category->id);
|
||||
$categorylist = [$category->id];
|
||||
}
|
||||
|
||||
// Get the list of questions for the category
|
||||
// Get the list of questions for the category.
|
||||
list($usql, $params) = $DB->get_in_or_equal($categorylist);
|
||||
$questions = $DB->get_records_select('question', "category {$usql} {$npsql}", $params, 'category, qtype, name');
|
||||
|
||||
// Iterate through questions, getting stuff we need
|
||||
$qresults = array();
|
||||
// Get the latest version of a question.
|
||||
$version = '';
|
||||
if ($latestversion) {
|
||||
$version = 'AND (qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be
|
||||
ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qbe.id) OR qv.version is null)';
|
||||
}
|
||||
$questions = $DB->get_records_sql("SELECT q.*, qv.status, qc.id AS category
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.id {$usql} {$npsql} {$version}
|
||||
ORDER BY qc.id, q.qtype, q.name", $params);
|
||||
|
||||
// Iterate through questions, getting stuff we need.
|
||||
$qresults = [];
|
||||
foreach($questions as $key => $question) {
|
||||
$question->export_process = $export;
|
||||
$qtype = question_bank::get_qtype($question->qtype, false);
|
||||
if ($export && $qtype->name() == 'missingtype') {
|
||||
if ($export && $qtype->name() === 'missingtype') {
|
||||
// Unrecognised question type. Skip this question when exporting.
|
||||
continue;
|
||||
}
|
||||
|
@ -307,7 +327,7 @@ function question_build_edit_resources($edittab, $baseurl, $params) {
|
|||
}
|
||||
|
||||
if ($thiscontext){
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
|
||||
$contexts->require_one_edit_tab_cap($edittab);
|
||||
} else {
|
||||
$contexts = null;
|
||||
|
|
|
@ -419,8 +419,10 @@ abstract class question_bank {
|
|||
|
||||
list($categorysql, $params) = $DB->get_in_or_equal($categories);
|
||||
$sql = "SELECT DISTINCT q.qtype
|
||||
FROM {question} q
|
||||
WHERE q.category $categorysql";
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid $categorysql";
|
||||
|
||||
$qtypes = $DB->get_fieldset_sql($sql, $params);
|
||||
return $qtypes;
|
||||
|
@ -454,7 +456,7 @@ class question_finder implements cache_data_source {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return get the question definition cache we are using.
|
||||
* @return cache_application the question definition cache we are using.
|
||||
*/
|
||||
protected function get_data_cache() {
|
||||
// Do not double cache here because it may break cache resetting.
|
||||
|
@ -496,12 +498,17 @@ class question_finder implements cache_data_source {
|
|||
if ($extraconditions) {
|
||||
$extraconditions = ' AND (' . $extraconditions . ')';
|
||||
}
|
||||
$qcparams['readystatus'] = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$sql = "SELECT q.id, q.id AS id2
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid {$qcsql}
|
||||
AND q.parent = 0
|
||||
AND qv.status = :readystatus
|
||||
{$extraconditions}";
|
||||
|
||||
return $DB->get_records_select_menu('question',
|
||||
"category {$qcsql}
|
||||
AND parent = 0
|
||||
AND hidden = 0
|
||||
{$extraconditions}", $qcparams + $extraparams, '', 'id,id AS id2');
|
||||
return $DB->get_records_sql_menu($sql, $qcparams + $extraparams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -543,14 +550,23 @@ class question_finder implements cache_data_source {
|
|||
|
||||
list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
|
||||
|
||||
$readystatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$select = "q.id, (SELECT COUNT(1)
|
||||
FROM " . $qubaids->from_question_attempts('qa') . "
|
||||
WHERE qa.questionid = q.id AND " . $qubaids->where() . "
|
||||
) AS previous_attempts";
|
||||
$from = "{question} q";
|
||||
$where = "q.category {$qcsql}
|
||||
$join = "JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid";
|
||||
$from = $from . " " . $join;
|
||||
$where = "qbe.questioncategoryid {$qcsql}
|
||||
AND q.parent = 0
|
||||
AND q.hidden = 0";
|
||||
AND qv.status = '$readystatus'
|
||||
AND qv.version = (SELECT MAX(v.version)
|
||||
FROM {question_versions} v
|
||||
JOIN {question_bank_entries} be
|
||||
ON be.id = v.questionbankentryid
|
||||
WHERE be.id = qbe.id)";
|
||||
$params = $qcparams;
|
||||
|
||||
if (!empty($tagids)) {
|
||||
|
@ -580,20 +596,32 @@ class question_finder implements cache_data_source {
|
|||
}
|
||||
|
||||
return $DB->get_records_sql_menu("SELECT $select
|
||||
FROM $from
|
||||
WHERE $where $extraconditions
|
||||
ORDER BY previous_attempts",
|
||||
FROM $from
|
||||
WHERE $where $extraconditions
|
||||
ORDER BY previous_attempts",
|
||||
$qubaids->from_where_params() + $params + $extraparams);
|
||||
}
|
||||
|
||||
/* See cache_data_source::load_for_cache. */
|
||||
public function load_for_cache($questionid) {
|
||||
global $DB;
|
||||
$questiondata = $DB->get_record_sql('
|
||||
SELECT q.*, qc.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
|
||||
|
||||
$sql = 'SELECT q.id, qc.id as category, q.parent, q.name, q.questiontext, q.questiontextformat,
|
||||
q.generalfeedback, q.generalfeedbackformat, q.defaultmark, q.penalty, q.qtype,
|
||||
q.length, q.stamp, q.timecreated, q.timemodified,
|
||||
q.createdby, q.modifiedby, qbe.idnumber,
|
||||
qc.contextid,
|
||||
qv.status,
|
||||
qv.id as versionid,
|
||||
qv.version,
|
||||
qv.questionbankentryid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.id = :id';
|
||||
|
||||
$questiondata = $DB->get_record_sql($sql, ['id' => $questionid], MUST_EXIST);
|
||||
get_question_options($questiondata);
|
||||
return $questiondata;
|
||||
}
|
||||
|
@ -602,15 +630,26 @@ class question_finder implements cache_data_source {
|
|||
public function load_many_for_cache(array $questionids) {
|
||||
global $DB;
|
||||
list($idcondition, $params) = $DB->get_in_or_equal($questionids);
|
||||
$questiondata = $DB->get_records_sql('
|
||||
SELECT q.*, qc.contextid
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
WHERE q.id ' . $idcondition, $params);
|
||||
$sql = 'SELECT q.id, qc.id as category, q.parent, q.name, q.questiontext, q.questiontextformat,
|
||||
q.generalfeedback, q.generalfeedbackformat, q.defaultmark, q.penalty, q.qtype,
|
||||
q.length, q.stamp, q.timecreated, q.timemodified,
|
||||
q.createdby, q.modifiedby, qbe.idnumber,
|
||||
qc.contextid,
|
||||
qv.status,
|
||||
qv.id as versionid,
|
||||
qv.version,
|
||||
qv.questionbankentryid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.id ';
|
||||
|
||||
$questiondata = $DB->get_records_sql($sql . $idcondition, $params);
|
||||
|
||||
foreach ($questionids as $id) {
|
||||
if (!array_key_exists($id, $questiondata)) {
|
||||
throw new dml_missing_record_exception('question', '', array('id' => $id));
|
||||
throw new dml_missing_record_exception('question', '', ['id' => $id]);
|
||||
}
|
||||
get_question_options($questiondata[$id]);
|
||||
}
|
||||
|
|
|
@ -117,12 +117,13 @@ abstract class question_test_helper {
|
|||
*/
|
||||
public static function get_question_editing_form($cat, $questiondata) {
|
||||
$catcontext = context::instance_by_id($cat->contextid, MUST_EXIST);
|
||||
$contexts = new question_edit_contexts($catcontext);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($catcontext);
|
||||
$dataforformconstructor = new stdClass();
|
||||
$dataforformconstructor->createdby = $questiondata->createdby;
|
||||
$dataforformconstructor->qtype = $questiondata->qtype;
|
||||
$dataforformconstructor->contextid = $questiondata->contextid = $catcontext->id;
|
||||
$dataforformconstructor->category = $questiondata->category = $cat->id;
|
||||
$dataforformconstructor->status = $questiondata->status;
|
||||
$dataforformconstructor->formoptions = new stdClass();
|
||||
$dataforformconstructor->formoptions->canmove = true;
|
||||
$dataforformconstructor->formoptions->cansaveasnew = true;
|
||||
|
@ -178,8 +179,8 @@ class test_question_maker {
|
|||
$q->penalty = 0.3333333;
|
||||
$q->length = 1;
|
||||
$q->stamp = make_unique_id_code();
|
||||
$q->version = make_unique_id_code();
|
||||
$q->hidden = 0;
|
||||
$q->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$q->version = 1;
|
||||
$q->timecreated = time();
|
||||
$q->timemodified = time();
|
||||
$q->createdby = $USER->id;
|
||||
|
@ -200,8 +201,8 @@ class test_question_maker {
|
|||
$qdata->penalty = 0.3333333;
|
||||
$qdata->length = 1;
|
||||
$qdata->stamp = make_unique_id_code();
|
||||
$qdata->version = make_unique_id_code();
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->version = 1;
|
||||
$qdata->timecreated = time();
|
||||
$qdata->timemodified = time();
|
||||
$qdata->createdby = $USER->id;
|
||||
|
|
|
@ -136,7 +136,7 @@ class qformat_default {
|
|||
*/
|
||||
public function setContexts($contexts) {
|
||||
$this->contexts = $contexts;
|
||||
$this->translator = new context_to_string_translator($this->contexts);
|
||||
$this->translator = new core_question\local\bank\context_to_string_translator($this->contexts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -411,8 +411,8 @@ class qformat_default {
|
|||
// Id number not really set. Get rid of it.
|
||||
unset($question->idnumber);
|
||||
} else {
|
||||
if ($DB->record_exists('question',
|
||||
['idnumber' => $question->idnumber, 'category' => $question->category])) {
|
||||
if ($DB->record_exists('question_bank_entries',
|
||||
['idnumber' => $question->idnumber, 'questioncategoryid' => $question->category])) {
|
||||
// We cannot have duplicate idnumbers in a category. Just remove it.
|
||||
unset($question->idnumber);
|
||||
}
|
||||
|
@ -426,6 +426,20 @@ class qformat_default {
|
|||
);
|
||||
|
||||
$question->id = $DB->insert_record('question', $question);
|
||||
// Create a bank entry for each question imported.
|
||||
$questionbankentry = new \stdClass();
|
||||
$questionbankentry->questioncategoryid = $question->category;
|
||||
$questionbankentry->idnumber = $question->idnumber ?? null;
|
||||
$questionbankentry->ownerid = $question->createdby;
|
||||
$questionbankentry->id = $DB->insert_record('question_bank_entries', $questionbankentry);
|
||||
// Create a version for each question imported.
|
||||
$questionversion = new \stdClass();
|
||||
$questionversion->questionbankentryid = $questionbankentry->id;
|
||||
$questionversion->questionid = $question->id;
|
||||
$questionversion->version = 1;
|
||||
$questionversion->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$questionversion->id = $DB->insert_record('question_versions', $questionversion);
|
||||
|
||||
$event = \core\event\question_created::create_from_question_instance($question, $this->importcontext);
|
||||
$event->trigger();
|
||||
|
||||
|
@ -499,9 +513,6 @@ class qformat_default {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Give the question a unique version stamp determined by question_hash()
|
||||
$DB->set_field('question', 'version', question_hash($question),
|
||||
array('id' => $question->id));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -886,7 +897,8 @@ class qformat_default {
|
|||
// get the questions (from database) in this category
|
||||
// only get q's with no parents (no cloze subquestions specifically)
|
||||
if ($this->category) {
|
||||
$questions = get_questions_category($this->category, true);
|
||||
// Export only the latest version of a question.
|
||||
$questions = get_questions_category($this->category, true, true, true, true);
|
||||
} else {
|
||||
$questions = $this->questions;
|
||||
}
|
||||
|
@ -907,9 +919,16 @@ class qformat_default {
|
|||
|
||||
foreach ($questions as $question) {
|
||||
// used by file api
|
||||
$contextid = $DB->get_field('question_categories', 'contextid',
|
||||
array('id' => $question->category));
|
||||
$questionbankentry = question_bank::load_question($question->id);
|
||||
$qcategory = $questionbankentry->category;
|
||||
$contextid = $DB->get_field('question_categories', 'contextid', ['id' => $qcategory]);
|
||||
$question->contextid = $contextid;
|
||||
$question->idnumber = $questionbankentry->idnumber;
|
||||
if ($question->status === \core_question\local\bank\question_version_status::QUESTION_STATUS_READY) {
|
||||
$question->status = 0;
|
||||
} else {
|
||||
$question->status = 1;
|
||||
}
|
||||
|
||||
// do not export hidden questions
|
||||
if (!empty($question->hidden)) {
|
||||
|
@ -934,7 +953,7 @@ class qformat_default {
|
|||
// If parent wasn't written.
|
||||
if (!in_array($trackcategoryparent, $writtencategories)) {
|
||||
// If parent is empty.
|
||||
if (!count($DB->get_records('question', array('category' => $trackcategoryparent)))) {
|
||||
if (!count($DB->get_records('question_bank_entries', ['questioncategoryid' => $trackcategoryparent]))) {
|
||||
$categoryname = $this->get_category_path($trackcategoryparent, $this->contexttofile);
|
||||
$categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategoryparent),
|
||||
'name, info, infoformat, idnumber', MUST_EXIST);
|
||||
|
|
|
@ -67,6 +67,10 @@ class qformat_multianswer extends qformat_default {
|
|||
$question->generalfeedbackformat = FORMAT_MOODLE;
|
||||
$question->length = 1;
|
||||
$question->penalty = 0.3333333;
|
||||
$question->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$question->version = 1;
|
||||
$question->versionid = 0;
|
||||
$question->questionbankentryid = 0;
|
||||
|
||||
if (!empty($question)) {
|
||||
$question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
|
||||
|
|
|
@ -1182,6 +1182,7 @@ class qformat_xml extends qformat_default {
|
|||
$invalidquestion = false;
|
||||
$fs = get_file_storage();
|
||||
$contextid = $question->contextid;
|
||||
$question->status = 0;
|
||||
// Get files used by the questiontext.
|
||||
$question->questiontextfiles = $fs->get_area_files(
|
||||
$contextid, 'question', 'questiontext', $question->id);
|
||||
|
@ -1205,7 +1206,10 @@ class qformat_xml extends qformat_default {
|
|||
// Check question type.
|
||||
$questiontype = $this->get_qtype($question->qtype);
|
||||
|
||||
$idnumber = htmlspecialchars($question->idnumber);
|
||||
$idnumber = '';
|
||||
if (isset($question->idnumber)) {
|
||||
$idnumber = htmlspecialchars($question->idnumber);
|
||||
}
|
||||
|
||||
// Categories are a special case.
|
||||
if ($question->qtype == 'category') {
|
||||
|
@ -1242,7 +1246,7 @@ class qformat_xml extends qformat_default {
|
|||
$expout .= " <defaultgrade>{$question->defaultmark}</defaultgrade>\n";
|
||||
}
|
||||
$expout .= " <penalty>{$question->penalty}</penalty>\n";
|
||||
$expout .= " <hidden>{$question->hidden}</hidden>\n";
|
||||
$expout .= " <hidden>{$question->status}</hidden>\n";
|
||||
$expout .= " <idnumber>{$idnumber}</idnumber>\n";
|
||||
|
||||
// The rest of the output depends on question type.
|
||||
|
|
|
@ -132,7 +132,13 @@ class qformat_xml_import_export_test extends advanced_testcase {
|
|||
*/
|
||||
public function assert_question_in_category($qname, $catname) {
|
||||
global $DB;
|
||||
$question = $DB->get_record('question', ['name' => $qname], '*', MUST_EXIST);
|
||||
|
||||
$sql = "SELECT q.*, qbe.questioncategoryid AS category
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE q.name = :name";
|
||||
$question = $DB->get_record_sql($sql, ['name' => $qname], MUST_EXIST);
|
||||
$category = $DB->get_record('question_categories', ['name' => $catname], '*', MUST_EXIST);
|
||||
$this->assertEquals($category->id, $question->category);
|
||||
}
|
||||
|
|
|
@ -57,8 +57,7 @@ class qformat_xml_test extends question_testcase {
|
|||
$q->penalty = 0.3333333;
|
||||
$q->length = 1;
|
||||
$q->stamp = make_unique_id_code();
|
||||
$q->version = make_unique_id_code();
|
||||
$q->hidden = 0;
|
||||
$q->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$q->timecreated = time();
|
||||
$q->timemodified = time();
|
||||
$q->createdby = $USER->id;
|
||||
|
@ -342,7 +341,7 @@ END;
|
|||
$qdata->defaultmark = 0;
|
||||
$qdata->length = 0;
|
||||
$qdata->penalty = 0;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
|
||||
$exporter = new qformat_xml();
|
||||
|
@ -570,7 +569,7 @@ END;
|
|||
$qdata->defaultmark = 1;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 0;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
$qdata->options = new stdClass();
|
||||
$qdata->options->id = 456;
|
||||
|
@ -742,7 +741,7 @@ END;
|
|||
$qdata->defaultmark = 1;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 0.3333333;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
|
@ -975,7 +974,7 @@ END;
|
|||
$qdata->defaultmark = 2;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 0.3333333;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
|
@ -1152,7 +1151,7 @@ END;
|
|||
$qdata->defaultmark = 1;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 0.1;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
|
@ -1284,7 +1283,7 @@ END;
|
|||
$qdata->defaultmark = 1;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 0.3333333;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
|
@ -1460,7 +1459,7 @@ END;
|
|||
$qdata->defaultmark = 1;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 1;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = null;
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
|
@ -1520,7 +1519,7 @@ END;
|
|||
$qdata->defaultmark = 1;
|
||||
$qdata->length = 1;
|
||||
$qdata->penalty = 1;
|
||||
$qdata->hidden = 0;
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$qdata->idnumber = 'TestIDNum2';
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
|
|
|
@ -57,9 +57,15 @@ function core_question_output_fragment_tags_form($args) {
|
|||
$filtercourses = null;
|
||||
}
|
||||
|
||||
$category = $DB->get_record('question_categories', ['id' => $question->category]);
|
||||
$sql = "SELECT qc.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE q.id = :id";
|
||||
$category = $DB->get_record_sql($sql, ['id' => $question->id]);
|
||||
$questioncontext = \context::instance_by_id($category->contextid);
|
||||
$contexts = new \question_edit_contexts($editingcontext);
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($editingcontext);
|
||||
|
||||
// Load the question tags and filter the course tags by the current course.
|
||||
if (core_tag_tag::is_enabled('core_question', 'question')) {
|
||||
|
|
|
@ -144,7 +144,14 @@ class core_question_backup_testcase extends advanced_testcase {
|
|||
|
||||
// The questions should remain in the question category they were which is
|
||||
// a question category belonging to a course category context.
|
||||
$questions = $DB->get_records('question', ['category' => $qcat->id], 'idnumber');
|
||||
$sql = 'SELECT q.*,
|
||||
qbe.idnumber
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.questioncategoryid = ?
|
||||
ORDER BY qbe.idnumber';
|
||||
$questions = $DB->get_records_sql($sql, [$qcat->id]);
|
||||
$this->assertCount(2, $questions);
|
||||
|
||||
// Retrieve tags for each question and check if they are assigned at the right context.
|
||||
|
@ -190,9 +197,11 @@ class core_question_backup_testcase extends advanced_testcase {
|
|||
|
||||
// The questions should have been moved to a question category that belongs to a course context.
|
||||
$questions = $DB->get_records_sql("SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
WHERE qc.contextid = ?", [$coursecontext3->id]);
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?", [$coursecontext3->id]);
|
||||
$this->assertCount(2, $questions);
|
||||
|
||||
// Now, retrieve tags for each question and check if they are assigned at the right context.
|
||||
|
|
|
@ -49,7 +49,7 @@ class core_question_bank_view_testcase extends advanced_testcase {
|
|||
$context = context_course::instance($course->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$contexts = new question_edit_contexts($context);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($context);
|
||||
$cat = question_make_default_categories($contexts->all());
|
||||
$questiondata = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $cat->id]);
|
||||
|
@ -93,7 +93,7 @@ class core_question_bank_view_testcase extends advanced_testcase {
|
|||
$context = context_course::instance($course->id);
|
||||
|
||||
// Create a question in the default category.
|
||||
$contexts = new question_edit_contexts($context);
|
||||
$contexts = new core_question\local\bank\question_edit_contexts($context);
|
||||
$cat = question_make_default_categories($contexts->all());
|
||||
$questiondata = $questiongenerator->create_question('numerical', null,
|
||||
['name' => 'Example question', 'category' => $cat->id]);
|
||||
|
|
|
@ -32,7 +32,6 @@ Feature: A teacher can duplicate questions in the question bank
|
|||
And I press "id_submitbutton"
|
||||
Then I should see "Duplicated question name"
|
||||
And I should see "Test question to be copied"
|
||||
And "Duplicated question name" row "Last modified by" column of "categoryquestions" table should contain "Teacher One"
|
||||
And "Test question to be copied ID number qid" row "Created by" column of "categoryquestions" table should contain "Admin User"
|
||||
|
||||
Scenario: Duplicated questions automatically get a new name suggested
|
||||
|
|
55
question/tests/behat/edit_question_versioning.feature
Normal file
55
question/tests/behat/edit_question_versioning.feature
Normal file
|
@ -0,0 +1,55 @@
|
|||
@core @core_question
|
||||
Feature: Questions in the question bank have versions
|
||||
In order to see modified questions
|
||||
As a teacher
|
||||
I want to view them as different versions
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Quiz 1 | C1 | quiz1 |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext | answer 1 |
|
||||
| Test questions | truefalse | First question | Answer the first question | True |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@javascript
|
||||
Scenario: Question version is displayed
|
||||
When I navigate to "Question bank" in current page administration
|
||||
And I should see "First question"
|
||||
And I click on "Edit" "link" in the "First question" "table_row"
|
||||
And I follow "Edit question"
|
||||
Then I should see "Version 1"
|
||||
|
||||
@javascript
|
||||
Scenario: Question version change when question is altered
|
||||
When I navigate to "Question bank" in current page administration
|
||||
And I should see "First question"
|
||||
And I click on "Edit" "link" in the "First question" "table_row"
|
||||
And I follow "Edit question"
|
||||
And I should see "Version 1"
|
||||
And I set the field "id_name" to "Renamed question v2"
|
||||
And I set the field "id_questiontext" to "edited question"
|
||||
And I press "id_submitbutton"
|
||||
And I should not see "First question"
|
||||
And I should see "Renamed question v2"
|
||||
And I click on "Edit" "link" in the "Renamed question v2" "table_row"
|
||||
And I follow "Edit question"
|
||||
Then I should see "Version 2"
|
||||
And I should not see "Version 1"
|
|
@ -32,8 +32,7 @@ Feature: A teacher can edit questions in the question bank
|
|||
And I press "id_submitbutton"
|
||||
Then I should see "Edited question name"
|
||||
And I should not see "Test question to be edited"
|
||||
And "Edited question name" row "Created by" column of "categoryquestions" table should contain "Admin User"
|
||||
And "Edited question name" row "Last modified by" column of "categoryquestions" table should contain "Teacher 1"
|
||||
And "Edited question name" row "Created by" column of "categoryquestions" table should contain "Teacher 1"
|
||||
|
||||
Scenario: Editing a question can be cancelled
|
||||
When I choose "Edit question" action for "Test question to be edited" in the question bank
|
||||
|
@ -41,7 +40,6 @@ Feature: A teacher can edit questions in the question bank
|
|||
And I press "Cancel"
|
||||
Then I should see "Test question to be edited"
|
||||
And "Test question to be edited" row "Created by" column of "categoryquestions" table should contain "Admin User"
|
||||
And "Test question to be edited" row "Last modified by" column of "categoryquestions" table should contain "Admin User"
|
||||
|
||||
Scenario: A question can have its idnumber removed
|
||||
Given the following "questions" exist:
|
||||
|
|
|
@ -39,16 +39,3 @@ Feature: A teacher can move questions between categories in the question bank
|
|||
And the field "Select a category" matches value " Subcategory (1)"
|
||||
And the "Select a category" select box should contain "Used category"
|
||||
And the "Select a category" select box should not contain "Used category (1)"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a question between categories via the question settings page
|
||||
When I navigate to "Question bank" in current page administration
|
||||
And I set the field "Select a category" to "Used category"
|
||||
And I choose "Edit question" action for "Test question to be moved" in the question bank
|
||||
And I click on "Use this category" "checkbox"
|
||||
And I set the field "Save in category" to "Subcategory"
|
||||
And I press "id_submitbutton"
|
||||
Then I should see "Test question to be moved"
|
||||
And the field "Select a category" matches value " Subcategory (1)"
|
||||
And the "Select a category" select box should contain "Used category"
|
||||
And the "Select a category" select box should not contain "Used category (1)"
|
||||
|
|
|
@ -46,7 +46,7 @@ Feature: A teacher can put questions with idnumbers in categories in the questio
|
|||
And I press "Save changes"
|
||||
Then I should not see "This ID number is already in use"
|
||||
|
||||
Scenario: Question idnumber conflicts found when saving to a different category.
|
||||
Scenario: Question idnumber conflicts found when saving to the same category.
|
||||
When the following "question categories" exist:
|
||||
| contextlevel | reference | questioncategory | name |
|
||||
| Course | C1 | Top | top |
|
||||
|
@ -55,13 +55,10 @@ Feature: A teacher can put questions with idnumbers in categories in the questio
|
|||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext | idnumber |
|
||||
| Category 1 | essay | Question to edit | Write about whatever you want | q1 |
|
||||
| Category 2 | essay | Other question | Write about whatever you want | q2 |
|
||||
| Category 1 | essay | Other question | Write about whatever you want | q2 |
|
||||
And I navigate to "Question bank" in current page administration
|
||||
And I choose "Edit question" action for "Question to edit" in the question bank
|
||||
And I set the following fields to these values:
|
||||
| Use this category | 0 |
|
||||
| ID number | q2 |
|
||||
| Save in category | Category 2 |
|
||||
And I set the field "ID number" to "q2"
|
||||
And I press "Save changes"
|
||||
Then I should see "This ID number is already in use"
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ use qbank_managecategories\question_category_object;
|
|||
use qtype_description;
|
||||
use qtype_description_edit_form;
|
||||
use qtype_description_test_helper;
|
||||
use question_edit_contexts;
|
||||
use test_question_maker;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
@ -57,7 +56,7 @@ class events_test extends \advanced_testcase {
|
|||
$course = $this->getDataGenerator()->create_course();
|
||||
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
|
||||
|
||||
$contexts = new question_edit_contexts(\context_module::instance($quiz->cmid));
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts(\context_module::instance($quiz->cmid));
|
||||
|
||||
$defaultcategoryobj = question_make_default_categories([$contexts->lowest()]);
|
||||
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
|
||||
|
@ -108,7 +107,7 @@ class events_test extends \advanced_testcase {
|
|||
$course = $this->getDataGenerator()->create_course();
|
||||
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]);
|
||||
|
||||
$contexts = new question_edit_contexts(\context_module::instance($quiz->cmid));
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts(\context_module::instance($quiz->cmid));
|
||||
|
||||
$defaultcategoryobj = question_make_default_categories([$contexts->lowest()]);
|
||||
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
|
||||
|
@ -229,12 +228,13 @@ class events_test extends \advanced_testcase {
|
|||
|
||||
// Trigger and capture the event.
|
||||
$sink = $this->redirectEvents();
|
||||
$qtype->save_question($questiondata, $fromform);
|
||||
$question = $qtype->save_question($questiondata, $fromform);
|
||||
$events = $sink->get_events();
|
||||
$event = reset($events);
|
||||
|
||||
// Check that the event data is valid.
|
||||
$this->assertInstanceOf('\core\event\question_updated', $event);
|
||||
// Every save is a new question after Moodle 4.0.
|
||||
$this->assertInstanceOf('\core\event\question_created', $event);
|
||||
$this->assertEquals($question->id, $event->objectid);
|
||||
$this->assertEquals($cat->id, $event->other['categoryid']);
|
||||
$this->assertDebuggingNotCalled();
|
||||
|
|
|
@ -14,16 +14,24 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Quiz module test data generator class
|
||||
* Quiz module test data generator.
|
||||
*
|
||||
* @package moodlecore
|
||||
* @subpackage question
|
||||
* @package core_question
|
||||
* @copyright 2013 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
|
||||
/**
|
||||
* Class core_question_generator for generating question data.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2013 The Open University
|
||||
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class core_question_generator extends component_generator_base {
|
||||
|
||||
/**
|
||||
|
@ -31,19 +39,23 @@ class core_question_generator extends component_generator_base {
|
|||
*/
|
||||
protected $categorycount = 0;
|
||||
|
||||
/**
|
||||
* Make the category count to zero.
|
||||
*/
|
||||
public function reset() {
|
||||
$this->categorycount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new question category.
|
||||
*
|
||||
* @param array|stdClass $record
|
||||
* @return stdClass question_categories record.
|
||||
*/
|
||||
public function create_question_category($record = null) {
|
||||
global $DB;
|
||||
|
||||
$this->categorycount++;
|
||||
$this->categorycount ++;
|
||||
|
||||
$defaults = [
|
||||
'name' => 'Test question category ' . $this->categorycount,
|
||||
|
@ -79,18 +91,11 @@ class core_question_generator extends component_generator_base {
|
|||
* @return stdClass the question data.
|
||||
*/
|
||||
public function create_question($qtype, $which = null, $overrides = null) {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, $which);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record(
|
||||
(array) $fromform, $overrides);
|
||||
|
||||
$question = new stdClass();
|
||||
$question->category = $fromform->category;
|
||||
$question->qtype = $qtype;
|
||||
$question->qtype = $qtype;
|
||||
$question->createdby = 0;
|
||||
$question->idnumber = null;
|
||||
$question->status = question_version_status::QUESTION_STATUS_READY;
|
||||
|
||||
return $this->update_question($question, $which, $overrides);
|
||||
}
|
||||
|
@ -122,10 +127,9 @@ class core_question_generator extends component_generator_base {
|
|||
$qtype = $question->qtype;
|
||||
|
||||
$fromform = test_question_maker::get_question_form_data($qtype, $which);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record(
|
||||
(array) $question, $fromform);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record(
|
||||
(array) $fromform, $overrides);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record((array) $question, $fromform);
|
||||
$fromform = (object) $this->datagenerator->combine_defaults_and_record((array) $fromform, $overrides);
|
||||
$fromform->status = $question->status;
|
||||
|
||||
$question = question_bank::get_qtype($qtype)->save_question($question, $fromform);
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ class generator_test extends \advanced_testcase {
|
|||
question_move_questions_to_category([$quest1->id], $qcat1->id);
|
||||
$this->assertSame('myquest', \question_bank::load_question_data($quest1->id)->idnumber);
|
||||
// Can only change idnumber of quest2 once quest1 has been moved to another category.
|
||||
$quest2 = $generator->update_question($questions[1], null, ['idnumber' => 'myquest']);
|
||||
$quest2 = $generator->update_question($questions[1], null, ['idnumber' => 'myquest_4']);
|
||||
question_move_questions_to_category([$quest2->id], $qcat1->id);
|
||||
$this->assertSame('myquest_4', \question_bank::load_question_data($quest2->id)->idnumber);
|
||||
// Check can add an idnumber of 0.
|
||||
|
|
|
@ -310,12 +310,14 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
|||
$q2 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
|
||||
$this->setUser($otheruser);
|
||||
$questiongenerator->update_question($q2);
|
||||
// When we update a question, a new question/version is created.
|
||||
$q2updated = $questiongenerator->update_question($q2);
|
||||
$q3 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
$q4 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
|
||||
$this->setUser($user);
|
||||
$questiongenerator->update_question($q3);
|
||||
// When we update a question, a new question/version is created.
|
||||
$q3updated = $questiongenerator->update_question($q3);
|
||||
$q5 = $questiongenerator->create_question('shortanswer', null, ['category' => $othercat->id]);
|
||||
|
||||
$approvedcontextlist = new \core_privacy\tests\request\approved_contextlist(
|
||||
|
@ -337,12 +339,12 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
|||
$this->assertEquals(0, $qrecord->createdby);
|
||||
$this->assertEquals(0, $qrecord->modifiedby);
|
||||
|
||||
$qrecord = $DB->get_record('question', ['id' => $q2->id]);
|
||||
$this->assertEquals(0, $qrecord->createdby);
|
||||
$qrecord = $DB->get_record('question', ['id' => $q2updated->id]);
|
||||
$this->assertEquals($otheruser->id, $qrecord->createdby);
|
||||
$this->assertEquals($otheruser->id, $qrecord->modifiedby);
|
||||
|
||||
$qrecord = $DB->get_record('question', ['id' => $q3->id]);
|
||||
$this->assertEquals($otheruser->id, $qrecord->createdby);
|
||||
$qrecord = $DB->get_record('question', ['id' => $q3updated->id]);
|
||||
$this->assertEquals(0, $qrecord->createdby);
|
||||
$this->assertEquals(0, $qrecord->modifiedby);
|
||||
|
||||
$qrecord = $DB->get_record('question', ['id' => $q4->id]);
|
||||
|
@ -461,9 +463,7 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
|||
|
||||
// User1 has created questions and user3 has edited them.
|
||||
$this->assertCount(2, $userlist);
|
||||
$this->assertEqualsCanonicalizing(
|
||||
[$user1->id, $user3->id],
|
||||
$userlist->get_userids());
|
||||
$this->assertEqualsCanonicalizing([$user1->id, $user3->id], $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -510,22 +510,24 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
|||
provider::delete_data_for_users($approveduserlist);
|
||||
|
||||
// Now, there should be no question related to user1 or user2 in course1.
|
||||
$this->assertEquals(
|
||||
0,
|
||||
$this->assertEquals(0,
|
||||
$DB->count_records_sql("SELECT COUNT(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ?
|
||||
AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
|
||||
AND (q.createdby = ? OR q.modifiedby = ? OR q.createdby = ? OR q.modifiedby = ?)",
|
||||
[$course1context->id, $user1->id, $user1->id, $user2->id, $user2->id])
|
||||
);
|
||||
|
||||
// User3 data in course1 should not change.
|
||||
$this->assertEquals(
|
||||
2,
|
||||
$this->assertEquals(2,
|
||||
$DB->count_records_sql("SELECT COUNT(q.id)
|
||||
FROM {question} q
|
||||
JOIN {question_categories} qc ON q.category = qc.id
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.contextid = ? AND (q.createdby = ? OR q.modifiedby = ?)",
|
||||
[$course1context->id, $user3->id, $user3->id])
|
||||
);
|
||||
|
|
|
@ -45,7 +45,7 @@ class question_bank_column_testcase extends advanced_testcase {
|
|||
$this->resetAfterTest();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$questionbank = new core_question\local\bank\view(
|
||||
new question_edit_contexts(context_course::instance($course->id)),
|
||||
new core_question\local\bank\question_edit_contexts(context_course::instance($course->id)),
|
||||
new moodle_url('/'),
|
||||
$course
|
||||
);
|
||||
|
@ -79,7 +79,7 @@ class question_bank_column_testcase extends advanced_testcase {
|
|||
$this->resetAfterTest();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$questionbank = new core_question\local\bank\view(
|
||||
new question_edit_contexts(context_course::instance($course->id)),
|
||||
new core_question\local\bank\question_edit_contexts(context_course::instance($course->id)),
|
||||
new moodle_url('/'),
|
||||
$course
|
||||
);
|
||||
|
|
|
@ -68,7 +68,8 @@ class random_question_loader_testcase extends advanced_testcase {
|
|||
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
$DB->set_field('question', 'hidden', 1, ['id' => $question1->id]);
|
||||
$DB->set_field('question_versions', 'status',
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $question1->id]);
|
||||
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
|
||||
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 0));
|
||||
|
|
257
question/tests/version_test.php
Normal file
257
question/tests/version_test.php
Normal file
|
@ -0,0 +1,257 @@
|
|||
<?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 core_question;
|
||||
|
||||
use question_bank;
|
||||
|
||||
/**
|
||||
* Question version unit tests.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2021 Catalyst IT Australia Pty Ltd
|
||||
* @author Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \question_bank
|
||||
*/
|
||||
class version_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* @var \context_module module context.
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @var \stdClass course object.
|
||||
*/
|
||||
protected $course;
|
||||
|
||||
/**
|
||||
* @var \component_generator_base question generator.
|
||||
*/
|
||||
protected $qgenerator;
|
||||
|
||||
/**
|
||||
* @var \stdClass quiz object.
|
||||
*/
|
||||
protected $quiz;
|
||||
|
||||
/**
|
||||
* Called before every test.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
self::setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
|
||||
$datagenerator = $this->getDataGenerator();
|
||||
$this->course = $datagenerator->create_course();
|
||||
$this->quiz = $datagenerator->create_module('quiz', ['course' => $this->course->id]);
|
||||
$this->qgenerator = $datagenerator->get_plugin_generator('core_question');
|
||||
$this->context = \context_module::instance($this->quiz->cmid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if creating a question a new version and bank entry records are created.
|
||||
*
|
||||
* @covers ::load_question
|
||||
*/
|
||||
public function test_make_question_create_version_and_bank_entry() {
|
||||
global $DB;
|
||||
|
||||
$qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
|
||||
|
||||
// Get the question object after creating a question.
|
||||
$questiondefinition = question_bank::load_question($question->id);
|
||||
|
||||
// The version and bank entry in the object should be the same.
|
||||
$sql = "SELECT qv.id AS versionid, qv.questionbankentryid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE q.id = ?";
|
||||
$questionversion = $DB->get_record_sql($sql, [$questiondefinition->id]);
|
||||
$this->assertEquals($questionversion->versionid, $questiondefinition->versionid);
|
||||
$this->assertEquals($questionversion->questionbankentryid, $questiondefinition->questionbankentryid);
|
||||
|
||||
// If a question is updated, a new version should be created.
|
||||
$this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
|
||||
$newquestiondefinition = question_bank::load_question($question->id);
|
||||
// The version should be 2.
|
||||
$this->assertEquals('2', $newquestiondefinition->version);
|
||||
|
||||
// Both versions should be in same bank entry.
|
||||
$this->assertEquals($questiondefinition->questionbankentryid, $newquestiondefinition->questionbankentryid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if deleting a question the related version and bank entry records are deleted.
|
||||
*
|
||||
* @covers ::load_question
|
||||
* @covers ::question_delete_question
|
||||
*/
|
||||
public function test_delete_question_delete_versions() {
|
||||
global $DB;
|
||||
|
||||
$qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
|
||||
$questionfirstversionid = $question->id;
|
||||
|
||||
// Create a new version and try to remove it.
|
||||
$this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
|
||||
|
||||
// The new version and bank entry record should exist.
|
||||
$sql = "SELECT q.id, qv.id AS versionid, qv.questionbankentryid
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE q.id = ?";
|
||||
$questionobject = $DB->get_records_sql($sql, [$question->id]);
|
||||
$this->assertCount(1, $questionobject);
|
||||
|
||||
// Try to delete new version.
|
||||
question_delete_question($question->id);
|
||||
|
||||
// The version record should not exist.
|
||||
$sql = "SELECT qv.*
|
||||
FROM {question_versions} qv
|
||||
WHERE qv.id = ?";
|
||||
$questionversion = $DB->get_record_sql($sql, [$questionobject[$question->id]->versionid]);
|
||||
$this->assertFalse($questionversion);
|
||||
|
||||
// The bank entry record should exist because there is an older version.
|
||||
$sql = "SELECT qbe.*
|
||||
FROM {question_bank_entries} qbe
|
||||
WHERE qbe.id = ?";
|
||||
$questionbankentry = $DB->get_records_sql($sql, [$questionobject[$question->id]->questionbankentryid]);
|
||||
$this->assertCount(1, $questionbankentry);
|
||||
|
||||
// Now remove the first version.
|
||||
question_delete_question($questionfirstversionid);
|
||||
$sql = "SELECT qbe.*
|
||||
FROM {question_bank_entries} qbe
|
||||
WHERE qbe.id = ?";
|
||||
$questionbankentry = $DB->get_record_sql($sql, [$questionobject[$question->id]->questionbankentryid]);
|
||||
// The bank entry record should not exist.
|
||||
$this->assertFalse($questionbankentry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if deleting a question will not break a quiz.
|
||||
*
|
||||
* @covers ::load_question
|
||||
* @covers ::quiz_add_quiz_question
|
||||
* @covers ::question_delete_question
|
||||
*/
|
||||
public function test_delete_question_in_use() {
|
||||
$qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategory->id]);
|
||||
$questionfirstversionid = $question->id;
|
||||
|
||||
// Create a new version and try to remove it after adding it to a quiz.
|
||||
$this->qgenerator->update_question($question, null, ['name' => 'This is a new version']);
|
||||
|
||||
// Add it to the quiz.
|
||||
quiz_add_quiz_question($question->id, $this->quiz);
|
||||
|
||||
// Try to delete new version.
|
||||
question_delete_question($question->id);
|
||||
// Try to delete old version.
|
||||
question_delete_question($questionfirstversionid);
|
||||
|
||||
// The questions should exist even after trying to remove it.
|
||||
$questionversion1 = question_bank::load_question($question->id);
|
||||
$questionversion2 = question_bank::load_question($questionfirstversionid);
|
||||
$this->assertEquals($questionversion1->id, $question->id);
|
||||
$this->assertEquals($questionversion2->id, $questionfirstversionid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if moving a category will not break a quiz.
|
||||
*
|
||||
* @covers ::load_question
|
||||
* @covers ::quiz_add_quiz_question
|
||||
*/
|
||||
public function test_move_category_with_questions() {
|
||||
global $DB;
|
||||
|
||||
$qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$qcategorychild = $this->qgenerator->create_question_category(['contextid' => $this->context->id,
|
||||
'parent' => $qcategory->id]);
|
||||
$systemcontext = \context_system::instance();
|
||||
$qcategorysys = $this->qgenerator->create_question_category(['contextid' => $systemcontext->id]);
|
||||
$question = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcategorychild->id]);
|
||||
$questiondefinition = question_bank::load_question($question->id);
|
||||
|
||||
// Add it to the quiz.
|
||||
quiz_add_quiz_question($question->id, $this->quiz);
|
||||
|
||||
// Move the category to system context.
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts($systemcontext);
|
||||
$qcobject = new \qbank_managecategories\question_category_object(null,
|
||||
new \moodle_url('/question/bank/managecategories/category.php', ['courseid' => SITEID]),
|
||||
$contexts->having_one_edit_tab_cap('categories'), 0, null, 0,
|
||||
$contexts->having_cap('moodle/question:add'));
|
||||
$qcobject->move_questions_and_delete_category($qcategorychild->id, $qcategorysys->id);
|
||||
|
||||
// The bank entry record should point to the new category in order to not break quizzes.
|
||||
$sql = "SELECT qbe.questioncategoryid
|
||||
FROM {question_bank_entries} qbe
|
||||
WHERE qbe.id = ?";
|
||||
$questionbankentry = $DB->get_record_sql($sql, [$questiondefinition->questionbankentryid]);
|
||||
$this->assertEquals($qcategorysys->id, $questionbankentry->questioncategoryid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that all versions will have the same bank entry idnumber value.
|
||||
*
|
||||
* @covers ::load_question
|
||||
*/
|
||||
public function test_id_number_in_bank_entry() {
|
||||
global $DB;
|
||||
|
||||
$qcategory = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
|
||||
$question = $this->qgenerator->create_question('shortanswer', null,
|
||||
[
|
||||
'category' => $qcategory->id,
|
||||
'idnumber' => 'id1'
|
||||
]);
|
||||
$questionid1 = $question->id;
|
||||
|
||||
// Create a new version and try to remove it after adding it to a quiz.
|
||||
$this->qgenerator->update_question($question, null, ['idnumber' => 'id2']);
|
||||
$questionid2 = $question->id;
|
||||
// Change the id number and get the question object.
|
||||
$this->qgenerator->update_question($question, null, ['idnumber' => 'id3']);
|
||||
$questionid3 = $question->id;
|
||||
|
||||
// The new version and bank entry record should exist.
|
||||
$questiondefinition = question_bank::load_question($question->id);
|
||||
$sql = "SELECT q.id AS questionid, qv.id AS versionid, qbe.id AS questionbankentryid, qbe.idnumber
|
||||
FROM {question_bank_entries} qbe
|
||||
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
||||
JOIN {question} q ON q.id = qv.questionid
|
||||
WHERE qbe.id = ?";
|
||||
$questionbankentry = $DB->get_records_sql($sql, [$questiondefinition->questionbankentryid]);
|
||||
|
||||
// We should have 3 versions and 1 question bank entry with the same idnumber.
|
||||
$this->assertCount(3, $questionbankentry);
|
||||
$this->assertEquals($questionbankentry[$questionid1]->idnumber, 'id3');
|
||||
$this->assertEquals($questionbankentry[$questionid2]->idnumber, 'id3');
|
||||
$this->assertEquals($questionbankentry[$questionid3]->idnumber, 'id3');
|
||||
}
|
||||
}
|
|
@ -56,16 +56,13 @@ class question_dataset_dependent_definitions_form extends question_wizard_form {
|
|||
* @param MoodleQuickForm $mform the form being built.
|
||||
*/
|
||||
public function __construct($submiturl, $question) {
|
||||
global $DB;
|
||||
// Validate the question category.
|
||||
if (!isset($question->categoryobject)) {
|
||||
throw new moodle_exception('categorydoesnotexist', 'question');
|
||||
}
|
||||
$question->category = $question->categoryobject->id;
|
||||
$this->question = $question;
|
||||
$this->qtypeobj = question_bank::get_qtype($this->question->qtype);
|
||||
// Validate the question category.
|
||||
if (!$category = $DB->get_record('question_categories',
|
||||
array('id' => $question->category))) {
|
||||
print_error('categorydoesnotexist', 'question', $returnurl);
|
||||
}
|
||||
$this->category = $category;
|
||||
$this->categorycontext = context::instance_by_id($category->contextid);
|
||||
parent::__construct($submiturl);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,17 +75,18 @@ class question_dataset_dependent_items_form extends question_wizard_form {
|
|||
* @param MoodleQuickForm $mform the form being built.
|
||||
*/
|
||||
public function __construct($submiturl, $question, $regenerate) {
|
||||
global $SESSION, $CFG, $DB;
|
||||
global $SESSION;
|
||||
|
||||
// Validate the question category.
|
||||
if (!isset($question->categoryobject)) {
|
||||
throw new moodle_exception('categorydoesnotexist', 'question');
|
||||
}
|
||||
$question->category = $question->categoryobject->id;
|
||||
$this->category = $question->categoryobject;
|
||||
$this->categorycontext = context::instance_by_id($this->category->contextid);
|
||||
$this->regenerate = $regenerate;
|
||||
$this->question = $question;
|
||||
$this->qtypeobj = question_bank::get_qtype($this->question->qtype);
|
||||
// Validate the question category.
|
||||
if (!$category = $DB->get_record('question_categories',
|
||||
array('id' => $question->category))) {
|
||||
print_error('categorydoesnotexist', 'question', $returnurl);
|
||||
}
|
||||
$this->category = $category;
|
||||
$this->categorycontext = context::instance_by_id($category->contextid);
|
||||
// Get the dataset defintions for this question.
|
||||
if (empty($question->id)) {
|
||||
$this->datasetdefs = $this->qtypeobj->get_dataset_definitions(
|
||||
|
|
|
@ -97,6 +97,7 @@ class qtype_calculated_test_helper extends question_test_helper {
|
|||
$qdata->name = 'Simple sum';
|
||||
$qdata->questiontext = 'What is {a} + {b}?';
|
||||
$qdata->generalfeedback = 'Generalfeedback: {={a} + {b}} is the right answer.';
|
||||
$qdata->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
|
||||
$qdata->options = new stdClass();
|
||||
$qdata->options->unitgradingtype = 0;
|
||||
|
@ -186,6 +187,8 @@ class qtype_calculated_test_helper extends question_test_helper {
|
|||
$fromform->feedback[2]['format'] = FORMAT_HTML;
|
||||
$fromform->feedback[2]['text'] = 'Completely wrong.';
|
||||
|
||||
$fromform->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
|
||||
return $fromform;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,8 +88,8 @@ class qtype_calculated_test extends advanced_testcase {
|
|||
|
||||
$this->assertEquals(['id', 'category', 'parent', 'name', 'questiontext', 'questiontextformat',
|
||||
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype',
|
||||
'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified',
|
||||
'createdby', 'modifiedby', 'idnumber', 'contextid', 'options', 'hints', 'categoryobject'],
|
||||
'length', 'stamp', 'timecreated', 'timemodified', 'createdby', 'modifiedby', 'idnumber', 'contextid',
|
||||
'status', 'versionid', 'version', 'questionbankentryid', 'categoryobject', 'options', 'hints'],
|
||||
array_keys(get_object_vars($questiondata)));
|
||||
$this->assertEquals($category->id, $questiondata->category);
|
||||
$this->assertEquals(0, $questiondata->parent);
|
||||
|
@ -102,7 +102,7 @@ class qtype_calculated_test extends advanced_testcase {
|
|||
$this->assertEquals(0, $questiondata->penalty);
|
||||
$this->assertEquals('calculated', $questiondata->qtype);
|
||||
$this->assertEquals(1, $questiondata->length);
|
||||
$this->assertEquals(0, $questiondata->hidden);
|
||||
$this->assertEquals(\core_question\local\bank\question_version_status::QUESTION_STATUS_READY, $questiondata->status);
|
||||
$this->assertEquals($question->createdby, $questiondata->createdby);
|
||||
$this->assertEquals($question->createdby, $questiondata->modifiedby);
|
||||
$this->assertEquals('', $questiondata->idnumber);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue