MDL-72321 mod_quiz: Use new datafilters for random questions

This commit is contained in:
Nathan Nguyen 2022-09-28 15:57:50 +10:00 committed by Mark Johnson
parent 823af3a8f1
commit e3bd166b3d
55 changed files with 1289 additions and 769 deletions

View File

@ -1,141 +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/>.
/**
* Fallback page of /mod/quiz/edit.php add random question dialog,
* for users who do not use javascript.
*
* @package mod_quiz
* @copyright 2008 Olli Savolainen
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/question/editlib.php');
use mod_quiz\form\add_random_form;
use mod_quiz\quiz_settings;
use qbank_managecategories\question_category_object;
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
question_edit_setup('editq', '/mod/quiz/addrandom.php', true);
// These params are only passed from page request to request while we stay on
// this page otherwise they would go in question_edit_setup.
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
$addonpage = optional_param('addonpage', 0, PARAM_INT);
$category = optional_param('category', 0, PARAM_INT);
$mdlscrollto = optional_param('mdlscrollto', 0, PARAM_INT);
$quizobj = quiz_settings::create($quiz->id);
$course = $quizobj->get_course();
// You need mod/quiz:manage in addition to question capabilities to access this page.
// You also need the moodle/question:useall capability somewhere.
require_capability('mod/quiz:manage', $contexts->lowest());
if (!$contexts->having_cap('moodle/question:useall')) {
throw new \moodle_exception('nopermissions', '', '', 'use');
}
$PAGE->set_url($thispageurl);
if ($returnurl) {
$returnurl = new moodle_url($returnurl);
} else {
$returnurl = new moodle_url('/mod/quiz/edit.php', ['cmid' => $cmid]);
}
if ($mdlscrollto) {
$returnurl->param('mdlscrollto', $mdlscrollto);
}
$defaultcategoryobj = question_make_default_categories($contexts->all());
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new question_category_object(
$pagevars['cpage'],
$thispageurl,
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
$mform = new add_random_form(new moodle_url('/mod/quiz/addrandom.php'),
['contexts' => $contexts, 'cat' => $pagevars['cat']]);
if ($mform->is_cancelled()) {
redirect($returnurl);
}
if ($data = $mform->get_data()) {
if (!empty($data->existingcategory)) {
list($categoryid) = explode(',', $data->category);
$includesubcategories = !empty($data->includesubcategories);
if (!$includesubcategories) {
// If the chosen category is a top category.
$includesubcategories = $DB->record_exists('question_categories', ['id' => $categoryid, 'parent' => 0]);
}
$returnurl->param('cat', $data->category);
} else if (!empty($data->newcategory)) {
list($parentid, $contextid) = explode(',', $data->parent);
$categoryid = $qcobject->add_category($data->parent, $data->name, '', true);
$includesubcategories = 0;
$returnurl->param('cat', $categoryid . ',' . $contextid);
} else {
throw new coding_exception(
'It seems a form was submitted without any button being pressed???');
}
if (empty($data->fromtags)) {
$data->fromtags = [];
}
$tagids = array_map(function($tagstrings) {
return (int)explode(',', $tagstrings)[0];
}, $data->fromtags);
quiz_add_random_questions($quiz, $addonpage, $categoryid, $data->numbertoadd, $includesubcategories, $tagids);
quiz_delete_previews($quiz);
$quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
redirect($returnurl);
}
$mform->set_data([
'addonpage' => $addonpage,
'returnurl' => $returnurl,
'cmid' => $cm->id,
'category' => $category,
]);
// Setup $PAGE.
$streditingquiz = get_string('editinga', 'moodle', get_string('modulename', 'quiz'));
$PAGE->navbar->add($streditingquiz);
$PAGE->set_title($streditingquiz);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
if (!$quizname = $DB->get_field($cm->modname, 'name', ['id' => $cm->instance])) {
throw new \moodle_exception('invalidcoursemodule');
}
echo $OUTPUT->heading(get_string('addrandomquestiontoquiz', 'quiz', $quizname), 2);
$mform->display();
echo $OUTPUT->footer();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
define("mod_quiz/modal_quiz_question_bank",["exports","jquery","core/modal","core/fragment","core_form/changechecker","core/modal_events"],(function(_exports,_jquery,_modal,Fragment,FormChangeChecker,ModalEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_modal=_interopRequireDefault(_modal),Fragment=_interopRequireWildcard(Fragment),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),ModalEvents=_interopRequireWildcard(ModalEvents);const SELECTORS_ADD_TO_QUIZ_CONTAINER="td.addtoquizaction",SELECTORS_ANCHOR="a[href]",SELECTORS_PREVIEW_CONTAINER="td.previewaction",SELECTORS_SEARCH_OPTIONS="#advancedsearch",SELECTORS_DISPLAY_OPTIONS="#displayoptions",SELECTORS_ADD_QUESTIONS_FORM="form#questionsubmit";class ModalQuizQuestionBank extends _modal.default{constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}show(){return this.reloadBodyContent(window.location.search),super.show(this)}reloadBodyContent(querystring){this.setBody(Fragment.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:querystring}))}handleAddToQuizEvent(e,anchorElement){const href=new URL(anchorElement.attr("href"));href.searchParams.set("addonpage",this.getAddOnPageId()),anchorElement.attr("href",href)}handlePreviewContainerEvent(e,anchorElement){window.openpopup(e,{url:anchorElement.attr("href"),name:"questionpreview",options:["height=600","width=800","top=0","left=0","menubar=0","location=0","scrollbars","resizable","toolbar","status","directories=0","fullscreen=0","dependent"].join(",")})}handleDisplayOptionFormEvent(e){e.stopPropagation(),e.preventDefault();const queryString="?"+(0,_jquery.default)(e.target).closest(SELECTORS_DISPLAY_OPTIONS).serialize();this.reloadBodyContent(queryString)}registerDisplayOptionListeners(){this.getModal().on("change",SELECTORS_DISPLAY_OPTIONS,(e=>{(0,_jquery.default)(e.target).attr("aria-autocomplete")||this.handleDisplayOptionFormEvent(e)})),this.getModal().on("submit",SELECTORS_DISPLAY_OPTIONS,(e=>{this.handleDisplayOptionFormEvent(e)}))}registerEventListeners(){super.registerEventListeners(this),this.registerDisplayOptionListeners(),this.getModal().on("submit",SELECTORS_ADD_QUESTIONS_FORM,(e=>{const formElement=(0,_jquery.default)(e.currentTarget);(0,_jquery.default)("<input />").attr("type","hidden").attr("name","addonpage").attr("value",this.getAddOnPageId()).appendTo(formElement)})),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=(0,_jquery.default)(e.currentTarget);anchorElement.closest(SELECTORS_ADD_TO_QUIZ_CONTAINER).length?this.handleAddToQuizEvent(e,anchorElement):anchorElement.closest(SELECTORS_PREVIEW_CONTAINER).length?this.handlePreviewContainerEvent(e,anchorElement):anchorElement.closest(SELECTORS_SEARCH_OPTIONS).length||(e.preventDefault(),this.reloadBodyContent(anchorElement.prop("search")))})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}var obj,key,value;return _exports.default=ModalQuizQuestionBank,value="mod_quiz-quiz-question-bank",(key="TYPE")in(obj=ModalQuizQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,ModalQuizQuestionBank.registerModalType(),_exports.default}));
define("mod_quiz/modal_quiz_question_bank",["exports","jquery","core/modal","core/fragment","core_form/changechecker","core/modal_events"],(function(_exports,_jquery,_modal,Fragment,FormChangeChecker,ModalEvents){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),_modal=_interopRequireDefault(_modal),Fragment=_interopRequireWildcard(Fragment),FormChangeChecker=_interopRequireWildcard(FormChangeChecker),ModalEvents=_interopRequireWildcard(ModalEvents);const SELECTORS_ADD_TO_QUIZ_CONTAINER="td.addtoquizaction",SELECTORS_ANCHOR="a[href]",SELECTORS_PREVIEW_CONTAINER="td.previewaction",SELECTORS_ADD_QUESTIONS_FORM="form#questionsubmit",SELECTORS_SORTERS=".sorters";class ModalQuizQuestionBank extends _modal.default{constructor(root){super(root),this.contextId=null,this.addOnPageId=null}setContextId(id){this.contextId=id}getContextId(){return this.contextId}setAddOnPageId(id){this.addOnPageId=id}getAddOnPageId(){return this.addOnPageId}show(){return this.reloadBodyContent(window.location.search),super.show(this)}reloadBodyContent(querystring){this.setBody(Fragment.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:querystring}))}handleAddToQuizEvent(e,anchorElement){const href=new URL(anchorElement.attr("href"));href.searchParams.set("addonpage",this.getAddOnPageId()),anchorElement.attr("href",href)}handlePreviewContainerEvent(e,anchorElement){window.openpopup(e,{url:anchorElement.attr("href"),name:"questionpreview",options:["height=600","width=800","top=0","left=0","menubar=0","location=0","scrollbars","resizable","toolbar","status","directories=0","fullscreen=0","dependent"].join(",")})}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("submit",SELECTORS_ADD_QUESTIONS_FORM,(e=>{const formElement=(0,_jquery.default)(e.currentTarget);(0,_jquery.default)("<input />").attr("type","hidden").attr("name","addonpage").attr("value",this.getAddOnPageId()).appendTo(formElement)})),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=(0,_jquery.default)(e.currentTarget);anchorElement.closest(SELECTORS_ADD_TO_QUIZ_CONTAINER).length?this.handleAddToQuizEvent(e,anchorElement):anchorElement.closest(SELECTORS_PREVIEW_CONTAINER).length?this.handlePreviewContainerEvent(e,anchorElement):anchorElement.closest(SELECTORS_SORTERS).length||(e.preventDefault(),this.reloadBodyContent(anchorElement.prop("search")))})),this.getRoot().on(ModalEvents.bodyRendered,(()=>{FormChangeChecker.disableAllChecks()}))}}var obj,key,value;return _exports.default=ModalQuizQuestionBank,value="mod_quiz-quiz-question-bank",(key="TYPE")in(obj=ModalQuizQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,ModalQuizQuestionBank.registerModalType(),_exports.default}));
//# sourceMappingURL=modal_quiz_question_bank.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
define("mod_quiz/update_random_question_filter_condition",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Event handling for the edit random question form.
*
* Dynamically saves the new filter condition before navigating back to the quiz question list.
*
* @module mod_quiz/update_random_question_filter_condition
* @author 2022 <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);_exports.init=()=>{const SELECTORS_QUESTION_BANK_CONTAINER="#questionbank_container",SELECTORS_FORM_ELEMENT="#update_filter_condition_form",SELECTORS_UPDATE_BUTTON='[name="update"]',SELECTORS_MESSAGE_INPUT='[name="message"]',SELECTORS_FILTER_CONDITION_ELEMENT="[data-filtercondition]",questionBank=document.querySelector(SELECTORS_QUESTION_BANK_CONTAINER),form=document.querySelector(SELECTORS_FORM_ELEMENT);form.querySelector(SELECTORS_UPDATE_BUTTON).addEventListener("click",(async e=>{var _form$dataset,_form$dataset2,_questionBank$querySe;e.preventDefault();const request={methodname:"mod_quiz_update_filter_condition",args:{cmid:null===(_form$dataset=form.dataset)||void 0===_form$dataset?void 0:_form$dataset.cmid,slotid:null===(_form$dataset2=form.dataset)||void 0===_form$dataset2?void 0:_form$dataset2.slotid,filtercondition:null===(_questionBank$querySe=questionBank.querySelector(SELECTORS_FILTER_CONDITION_ELEMENT).dataset)||void 0===_questionBank$querySe?void 0:_questionBank$querySe.filtercondition}};try{const response=await _ajax.default.call([request])[0];form.querySelector(SELECTORS_MESSAGE_INPUT).value=response.message,form.submit()}catch(e){_notification.default.exception(e)}}))}}));
//# sourceMappingURL=update_random_question_filter_condition.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"update_random_question_filter_condition.min.js","sources":["../src/update_random_question_filter_condition.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Event handling for the edit random question form.\n *\n * Dynamically saves the new filter condition before navigating back to the quiz question list.\n *\n * @module mod_quiz/update_random_question_filter_condition\n * @author 2022 <nathannguyen@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\nexport const init = () => {\n const SELECTORS = {\n QUESTION_BANK_CONTAINER: '#questionbank_container',\n FORM_ELEMENT: '#update_filter_condition_form',\n UPDATE_BUTTON: '[name=\"update\"]',\n CANCEL_BUTTON: '[name=\"cancel\"]',\n MESSAGE_INPUT: '[name=\"message\"]',\n FILTER_CONDITION_ELEMENT: '[data-filtercondition]',\n };\n\n const questionBank = document.querySelector(SELECTORS.QUESTION_BANK_CONTAINER);\n const form = document.querySelector(SELECTORS.FORM_ELEMENT);\n const updateButton = form.querySelector(SELECTORS.UPDATE_BUTTON);\n\n updateButton.addEventListener(\"click\", async(e) => {\n e.preventDefault();\n const request = {\n methodname: 'mod_quiz_update_filter_condition',\n args: {\n cmid: form.dataset?.cmid,\n slotid: form.dataset?.slotid,\n filtercondition: questionBank.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition,\n }\n };\n try {\n const response = await ajax.call([request])[0];\n const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);\n messageInput.value = response.message;\n form.submit();\n } catch (e) {\n Notification.exception(e);\n }\n });\n\n};\n"],"names":["SELECTORS","questionBank","document","querySelector","form","addEventListener","async","e","preventDefault","request","methodname","args","cmid","dataset","_form$dataset","slotid","_form$dataset2","filtercondition","_questionBank$querySe","response","ajax","call","value","message","submit","exception"],"mappings":";;;;;;;;;wLA4BoB,WACVA,kCACuB,0BADvBA,uBAEY,gCAFZA,wBAGa,kBAHbA,wBAKa,mBALbA,mCAMwB,yBAGxBC,aAAeC,SAASC,cAAcH,mCACtCI,KAAOF,SAASC,cAAcH,wBACfI,KAAKD,cAAcH,yBAE3BK,iBAAiB,SAASC,MAAAA,2DACnCC,EAAEC,uBACIC,QAAU,CACZC,WAAY,mCACZC,KAAM,CACFC,2BAAMR,KAAKS,wCAALC,cAAcF,KACpBG,8BAAQX,KAAKS,yCAALG,eAAcD,OACtBE,8CAAiBhB,aAAaE,cAAcH,oCAAoCa,gDAA/DK,sBAAwED,4BAIvFE,eAAiBC,cAAKC,KAAK,CAACZ,UAAU,GACvBL,KAAKD,cAAcH,yBAC3BsB,MAAQH,SAASI,QAC9BnB,KAAKoB,SACP,MAAOjB,yBACQkB,UAAUlB"}

View File

@ -27,18 +27,25 @@ import * as Notification from 'core/notification';
import * as Fragment from 'core/fragment';
import * as Templates from 'core/templates';
import * as FormChangeChecker from 'core_form/changechecker';
import {call as fetchMany} from 'core/ajax';
const SELECTORS = {
EXISTING_CATEGORY_CONTAINER: '[data-region="existing-category-container"]',
EXISTING_CATEGORY_FORM_ELEMENT: '#id_existingcategoryheader',
EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',
NEW_CATEGORY_CONTAINER: '[data-region="new-category-container"]',
NEW_CATEGORY_FORM_ELEMENT: '#id_newcategoryheader',
NEW_CATEGORY_TAB: '#id_newcategoryheader',
TAB_CONTENT: '[data-region="tab-content"]',
ADD_ON_PAGE_FORM_ELEMENT: '[name="addonpage"]',
SUBMIT_BUTTON_ELEMENT: 'input[type="submit"]',
CANCEL_BUTTON_ELEMENT: 'input[type="submit"][name="cancel"]',
ADD_RANDOM_BUTTON: 'input[type="submit"][name="addrandom"]',
ADD_NEW_CATEGORY_BUTTON: 'input[type="submit"][name="newcategory"]',
SUBMIT_BUTTON_ELEMENT: 'input[type="submit"][name="addrandom"], input[type="submit"][name="newcategory"]',
FORM_HEADER: 'legend',
BUTTON_CONTAINER: '.fitem',
SELECT_NUMBER_TO_ADD: '#menurandomcount',
NEW_CATEGORY_ELEMENT: '#categoryname',
PARENT_CATEGORY_ELEMENT: '#parentcategory',
FILTER_CONDITION_ELEMENT: '[data-filtercondition]',
FORM_ELEMENT: '#add_random_question_form',
MESSAGE_INPUT: '[name="message"]',
};
export default class ModalAddRandomQuestion extends Modal {
@ -173,24 +180,16 @@ export default class ModalAddRandomQuestion extends Modal {
* Moves the submit button into a footer element at the bottom of the form
* element for styling purposes.
*
* @method moveFormElementIntoTab
* @param {jquery} formElement The form element to move into the tab.
* @method moveContentIntoTab
* @param {jquery} tabContent The form element to move into the tab.
* @param {jquey} tabElement The tab element for the form element to move into.
*/
moveFormElementIntoTab(formElement, tabElement) {
const submitButtons = formElement.find(SELECTORS.SUBMIT_BUTTON_ELEMENT);
const footer = $('<div class="modal-footer mt-1" data-region="footer"></div>');
moveContentIntoTab(tabContent, tabElement) {
// Hide the header because the tabs show us which part of the form we're
// looking at.
formElement.find(SELECTORS.FORM_HEADER).addClass('hidden');
tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');
// Move the element inside a tab.
formElement.wrap(tabElement);
// Remove the buttons container element.
submitButtons.closest(SELECTORS.BUTTON_CONTAINER).remove();
// Put the button inside a footer.
submitButtons.appendTo(footer);
// Add the footer to the end of the category form element.
footer.appendTo(formElement);
tabContent.wrap(tabElement);
}
/**
@ -230,29 +229,33 @@ export default class ModalAddRandomQuestion extends Modal {
* @return {promise} Resolved with form HTML and JS.
*/
loadForm() {
const cmid = this.getCMID();
const cat = this.getCategory();
const addonpage = this.getAddOnPageId();
const returnurl = this.getReturnUrl();
return Fragment.loadFragment(
'mod_quiz',
'add_random_question_form',
this.getContextId(),
{
addonpage: this.getAddOnPageId(),
cat: this.getCategory(),
returnurl: this.getReturnUrl(),
cmid: this.getCMID()
addonpage,
cat,
returnurl,
cmid,
}
)
.then((html, js) =>{
const form = $(html);
const existingCategoryFormElement = form.find(SELECTORS.EXISTING_CATEGORY_FORM_ELEMENT);
const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);
const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);
const newCategoryFormElement = form.find(SELECTORS.NEW_CATEGORY_FORM_ELEMENT);
const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);
const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);
// Transform the form into tabs for better rendering in the modal.
this.moveFormElementIntoTab(existingCategoryFormElement, existingCategoryTab);
this.moveFormElementIntoTab(newCategoryFormElement, newCategoryTab);
this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);
this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);
this.moveTabsIntoTabContent(form);
this.moveCancelButtonToTabs(form);
Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);
return;
@ -261,11 +264,82 @@ export default class ModalAddRandomQuestion extends Modal {
// Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the
// page once the modal is hidden.
FormChangeChecker.disableAllChecks();
return;
// Add question to quiz.
this.getBody()[0].addEventListener('click', (e) => {
const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);
if (!button) {
return;
}
e.preventDefault();
const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;
const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;
// Add Random questions if the add random button was clicked.
const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);
if (addRandomButton) {
this.addQuestions(cmid, addonpage, randomcount, filtercondition, '', '');
return;
}
// Add new category if the add category button was clicked.
const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);
if (addCategoryButton) {
this.addQuestions(
cmid,
addonpage,
randomcount,
filtercondition,
document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,
document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value
);
return;
}
});
})
.catch(Notification.exception);
}
/**
* Call web service function to add random questions
*
* @param {number} cmid course module id
* @param {number} addonpage the page where random questions will be added to
* @param {number} randomcount Number of random questions
* @param {string} filtercondition Filter condition
* @param {string} newcategory add new category
* @param {string} parentcategory parent category of new category
*/
async addQuestions(
cmid,
addonpage,
randomcount,
filtercondition,
newcategory,
parentcategory
) {
const call = {
methodname: 'mod_quiz_add_random_questions',
args: {
cmid,
addonpage,
randomcount,
filtercondition,
newcategory,
parentcategory,
}
};
try {
const response = await fetchMany([call])[0];
const form = document.querySelector(SELECTORS.FORM_ELEMENT);
const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
messageInput.value = response.message;
form.submit();
} catch (e) {
Notification.exception(e);
}
}
/**
* Override the modal show function to load the form when this modal is first
* shown.
@ -276,7 +350,7 @@ export default class ModalAddRandomQuestion extends Modal {
super.show(this);
if (!this.loadedForm) {
this.loadForm();
this.loadForm(window.location.search);
this.loadedForm = true;
}
}

View File

@ -31,9 +31,8 @@ const SELECTORS = {
ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',
ANCHOR: 'a[href]',
PREVIEW_CONTAINER: 'td.previewaction',
SEARCH_OPTIONS: '#advancedsearch',
DISPLAY_OPTIONS: '#displayoptions',
ADD_QUESTIONS_FORM: 'form#questionsubmit',
SORTERS: '.sorters',
};
export default class ModalQuizQuestionBank extends Modal {
@ -178,59 +177,6 @@ export default class ModalQuizQuestionBank extends Modal {
});
}
/**
* Reload the modal body with the new display options the user has selected.
*
* A query string is built using the form elements to be used to generate the
* new body content.
*
* @method handleDisplayOptionFormEvent
* @param {event} e A JavaScript event
*/
handleDisplayOptionFormEvent(e) {
// Stop propagation to prevent other wild event handlers
// from submitting the form on change.
e.stopPropagation();
e.preventDefault();
const form = $(e.target).closest(SELECTORS.DISPLAY_OPTIONS);
const queryString = '?' + form.serialize();
this.reloadBodyContent(queryString);
}
/**
* Listen for changes to the display options form.
*
* This handles the user changing:
* - The quiz category select box
* - The tags to filter by
* - Show/hide questions from sub categories
* - Show/hide old questions
*
* @method registerDisplayOptionListeners
*/
registerDisplayOptionListeners() {
// Listen for changes to the display options form.
this.getModal().on('change', SELECTORS.DISPLAY_OPTIONS, (e) => {
// Get the element that was changed.
const modifiedElement = $(e.target);
if (modifiedElement.attr('aria-autocomplete')) {
// If the element that was change is the autocomplete
// input then we should ignore it because that is for
// display purposes only.
return;
}
this.handleDisplayOptionFormEvent(e);
});
// Listen for the display options form submission because the tags
// filter will submit the form when it is changed.
this.getModal().on('submit', SELECTORS.DISPLAY_OPTIONS, (e) => {
this.handleDisplayOptionFormEvent(e);
});
}
/**
* Set up all of the event handling for the modal.
*
@ -240,9 +186,6 @@ export default class ModalQuizQuestionBank extends Modal {
// Apply parent event listeners.
super.registerEventListeners(this);
// Set up the event handlers for all of the display options.
this.registerDisplayOptionListeners();
this.getModal().on('submit', SELECTORS.ADD_QUESTIONS_FORM, (e) => {
// If the user clicks on the "Add selected questions to the quiz" button to add some questions to the page
// then we need to intercept the submit in order to include the correct "add on page id" before the form is
@ -270,9 +213,8 @@ export default class ModalQuizQuestionBank extends Modal {
return;
}
// Click on expand/collaspse search-options. Has its own handler.
// We should not interfere.
if (anchorElement.closest(SELECTORS.SEARCH_OPTIONS).length) {
// Sorting links have their own handler.
if (anchorElement.closest(SELECTORS.SORTERS).length) {
return;
}

View File

@ -0,0 +1,63 @@
// 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/>.
/**
* Event handling for the edit random question form.
*
* Dynamically saves the new filter condition before navigating back to the quiz question list.
*
* @module mod_quiz/update_random_question_filter_condition
* @author 2022 <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
import Notification from 'core/notification';
export const init = () => {
const SELECTORS = {
QUESTION_BANK_CONTAINER: '#questionbank_container',
FORM_ELEMENT: '#update_filter_condition_form',
UPDATE_BUTTON: '[name="update"]',
CANCEL_BUTTON: '[name="cancel"]',
MESSAGE_INPUT: '[name="message"]',
FILTER_CONDITION_ELEMENT: '[data-filtercondition]',
};
const questionBank = document.querySelector(SELECTORS.QUESTION_BANK_CONTAINER);
const form = document.querySelector(SELECTORS.FORM_ELEMENT);
const updateButton = form.querySelector(SELECTORS.UPDATE_BUTTON);
updateButton.addEventListener("click", async(e) => {
e.preventDefault();
const request = {
methodname: 'mod_quiz_update_filter_condition',
args: {
cmid: form.dataset?.cmid,
slotid: form.dataset?.slotid,
filtercondition: questionBank.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition,
}
};
try {
const response = await ajax.call([request])[0];
const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
messageInput.value = response.message;
form.submit();
} catch (e) {
Notification.exception(e);
}
});
};

View File

@ -0,0 +1,158 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
use external_function_parameters;
use external_single_structure;
use external_value;
use external_api;
use mod_quiz\question\bank\filter\custom_category_condition;
use mod_quiz\quiz_settings;
use mod_quiz\structure;
/**
* Add random questions to a quiz.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class add_random_questions extends external_api {
/**
* Parameters for the web service function
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters (
[
'cmid' => new external_value(PARAM_INT, 'The cmid of the quiz'),
'addonpage' => new external_value(PARAM_INT, 'The page where random questions will be added to'),
'randomcount' => new external_value(PARAM_INT, 'Number of random questions'),
'filtercondition' => new external_value(PARAM_TEXT, 'Filter condition'),
'newcategory' => new external_value(
PARAM_TEXT,
'(Optional) The name of a new question category to create and use for the random questions.',
VALUE_DEFAULT,
'',
),
'parentcategory' => new external_value(
PARAM_TEXT,
'(Optional) The parent of the new question category, if creating one.',
VALUE_DEFAULT,
0,
),
]
);
}
/**
* Add random questions.
*
* @param int $cmid The cmid of the quiz
* @param int $addonpage The page where random questions will be added to
* @param int $randomcount Number of random questions
* @param string $filtercondition Filter condition
* @param string $newcategory add new category
* @param string $parentcategory parent category of new category
* @return array result
*/
public static function execute(
int $cmid,
int $addonpage,
int $randomcount,
string $filtercondition,
string $newcategory = '',
string $parentcategory = '',
): array {
[
'cmid' => $cmid,
'addonpage' => $addonpage,
'randomcount' => $randomcount,
'filtercondition' => $filtercondition,
'newcategory' => $newcategory,
'parentcategory' => $parentcategory,
] = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'addonpage' => $addonpage,
'randomcount' => $randomcount,
'filtercondition' => $filtercondition,
'newcategory' => $newcategory,
'parentcategory' => $parentcategory,
]);
// Validate context.
$thiscontext = \context_module::instance($cmid);
self::validate_context($thiscontext);
require_capability('mod/quiz:manage', $thiscontext);
$filtercondition = json_decode($filtercondition, true);
// Create new category.
if (!empty($newcategory)) {
$contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
$defaultcategoryobj = question_make_default_categories($contexts->all());
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
$qcobject = new \qbank_managecategories\question_category_object(
null,
null,
$contexts->having_one_edit_tab_cap('categories'),
$defaultcategoryobj->id,
$defaultcategory,
null,
$contexts->having_cap('moodle/question:add'));
$categoryid = $qcobject->add_category($parentcategory, $newcategory, '', true);
$filter = [
'category' => [
'jointype' => custom_category_condition::JOINTYPE_DEFAULT,
'values' => [$categoryid],
'filteroptions' => ['includesubcategories' => false],
]
];
$filtercondition['filter'] = $filter;
}
// Add random question to the quiz.
[$quiz, ] = get_module_from_cmid($cmid);
$settings = quiz_settings::create_for_cmid($cmid);
$structure = structure::create_for_quiz($settings);
$structure->add_random_questions($addonpage, $randomcount, $filtercondition);
quiz_delete_previews($quiz);
quiz_settings::create($quiz->id)->get_grade_calculator()->recompute_quiz_sumgrades();
return ['message' => get_string('addarandomquestion_success', 'mod_quiz')];
}
/**
* Returns description of method result value.
*
* @return external_value
*/
public static function execute_returns() {
return new external_single_structure([
'message' => new external_value(PARAM_TEXT, 'Message', VALUE_OPTIONAL)
]);
}
}

View File

@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\external;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
use external_function_parameters;
use external_single_structure;
use external_value;
use external_api;
/**
* Update the filter condition for a random question.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class update_filter_condition extends external_api {
/**
* Parameters for the web service function
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters ([
'cmid' => new external_value(PARAM_INT, 'The cmid of the quiz'),
'slotid' => new external_value(PARAM_INT, 'The quiz slot ID for the random question.'),
'filtercondition' => new external_value(PARAM_TEXT, 'Filter condition'),
]);
}
/**
* Add random questions.
*
* @param int $cmid course module id
* @param int $slotid The quiz slot id
* @param string $filtercondition
* @return array result
*/
public static function execute(
int $cmid,
int $slotid,
string $filtercondition,
): array {
global $DB;
[
'cmid' => $cmid,
'slotid' => $slotid,
'filtercondition' => $filtercondition,
] = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid,
'slotid' => $slotid,
'filtercondition' => $filtercondition,
]);
// Validate context.
$thiscontext = \context_module::instance($cmid);
self::validate_context($thiscontext);
require_capability('mod/quiz:manage', $thiscontext);
// Update filter condition.
$setparams = [
'itemid' => $slotid,
'questionarea' => 'slot',
'component' => 'mod_quiz',
];
$DB->set_field('question_set_references', 'filtercondition', $filtercondition, $setparams);
return ['message' => get_string('updatefilterconditon_success', 'mod_quiz')];
}
/**
* Returns description of method result value.
*
* @return external_value
*/
public static function execute_returns() {
return new external_single_structure([
'message' => new external_value(PARAM_TEXT, 'Message', VALUE_OPTIONAL)
]);
}
}

View File

@ -16,6 +16,7 @@
namespace mod_quiz\form;
use core\check\performance\debugging;
use core_tag_tag;
use moodleform;
@ -30,10 +31,23 @@ require_once($CFG->libdir.'/formslib.php');
* @package mod_quiz
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Moodle 4.3 MDL-72321. This form is new generated in a modal with mod_quiz/add_random_question_form.mustache
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
class add_random_form extends moodleform {
/**
* Deprecated.
*
* @return void
* @deprecated Moodle 4.3 MDL-72321
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
protected function definition() {
debugging(
'add_random_form is deprecated. Please use mod_quiz/add_random_question_form.mustache instead.',
DEBUG_DEVELOPER
);
global $OUTPUT, $PAGE, $CFG;
$mform = $this->_form;
@ -119,7 +133,20 @@ class add_random_form extends moodleform {
]);
}
/**
* Deprecated.
*
* @param array $fromform
* @param array $files
* @return array
* @deprecated Moodle 4.3 MDL-72321
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
public function validation($fromform, $files) {
debugging(
'add_random_form is deprecated. Please use mod_quiz/add_random_question_form.mustache instead.',
DEBUG_DEVELOPER
);
$errors = parent::validation($fromform, $files);
if (!empty($fromform['newcategory']) && trim($fromform['name']) == '') {

View File

@ -112,8 +112,13 @@ class slot_random {
* Set some tags for this quiz slot.
*
* @param \core_tag_tag[] $tags
*
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function set_tags($tags) {
debugging('Method set_tags() is deprecated, ' .
'please do not use this function.', DEBUG_DEVELOPER);
$this->tags = [];
foreach ($tags as $tag) {
// We use $tag->id as the key for the array so not only it handles duplicates of the same tag being given,
@ -126,22 +131,24 @@ class slot_random {
* Set some tags for this quiz slot. This function uses tag ids to find tags.
*
* @param int[] $tagids
* @deprecated since Moodle 4.3
* @todo Final deprecation on Moodle 4.7 MDL-78091
*/
public function set_tags_by_id($tagids) {
debugging(
'Method set_tags_by_id() is deprecated, please do not use this function.',
DEBUG_DEVELOPER
);
$this->tags = \core_tag_tag::get_bulk($tagids, 'id, name');
}
/**
* Set filter condition.
*
* @param \stdClass $filters
* @param \string $filters
*/
public function set_filter_condition($filters) {
if (!empty($this->tags)) {
$filters->tags = $this->tags;
}
$this->filtercondition = json_encode($filters);
public function set_filter_condition(string $filters): void {
$this->filtercondition = $filters;
}
/**

View File

@ -680,7 +680,7 @@ class edit_renderer extends \plugin_renderer_base {
if ($structure->can_add_random_questions()) {
$returnurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $structure->get_cmid(), 'data-addonpage' => $page]);
$params = ['returnurl' => $returnurl, 'cmid' => $structure->get_cmid(), 'appendqnumstring' => 'addarandomquestion'];
$url = new \moodle_url('/mod/quiz/addrandom.php', $params);
$url = new \moodle_url('/mod/quiz/edit.php', $params);
$icon = new \pix_icon('t/add', $str->addarandomquestion, 'moodle', ['class' => 'iconsmall', 'title' => '']);
$attributes = ['class' => 'cm-edit-action addarandomquestion', 'data-action' => 'addarandomquestion'];
if ($page) {
@ -1079,7 +1079,6 @@ class edit_renderer extends \plugin_renderer_base {
$qbankurlparams = [
'cmid' => $structure->get_cmid(),
'cat' => $slot->category . ',' . $slot->contextid,
'recurse' => $slot->randomrecurse,
];
$slottags = [];

View File

@ -25,9 +25,15 @@
namespace mod_quiz\question\bank;
defined('MOODLE_INTERNAL') || die();
use core\output\datafilter;
use core_question\local\bank\condition;
use core_question\local\bank\question_version_status;
use mod_quiz\question\bank\filter\custom_category_condition;
use qbank_managecategories\category_condition;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
/**
* Subclass to customise the view of the question bank for the quiz editing screen.
*
@ -45,8 +51,10 @@ class custom_view extends \core_question\local\bank\view {
/** @var \stdClass $quiz the quiz settings. */
protected $quiz = false;
/** @var int The maximum displayed length of the category info. */
const MAX_TEXT_LENGTH = 200;
/**
* @var string $component the component the api is used from.
*/
public $component = 'mod_quiz';
/**
* Constructor.
@ -56,27 +64,55 @@ class custom_view extends \core_question\local\bank\view {
* @param \stdClass $cm activity settings.
* @param \stdClass $quiz quiz settings.
*/
public function __construct($contexts, $pageurl, $course, $cm, $quiz) {
parent::__construct($contexts, $pageurl, $course, $cm);
$this->quiz = $quiz;
public function __construct($contexts, $pageurl, $course, $cm, $params, $extraparams) {
// Default filter condition.
if (!isset($params['filter'])) {
$params['filter'] = [];
[$categoryid, $contextid] = custom_category_condition::validate_category_param($params['cat']);
if (!is_null($categoryid)) {
$category = custom_category_condition::get_category_record($categoryid, $contextid);
$params['filter']['category'] = [
'jointype' => custom_category_condition::JOINTYPE_DEFAULT,
'values' => [$category->id],
'filteroptions' => ['includesubcategories' => false],
];
}
}
$this->init_required_columns();
parent::__construct($contexts, $pageurl, $course, $cm, $params, $extraparams);
[$this->quiz, ] = get_module_from_cmid($cm->id);
$this->set_quiz_has_attempts(quiz_has_attempts($this->quiz->id));
$this->pagesize = self::DEFAULT_PAGE_SIZE;
}
protected function get_question_bank_plugins(): array {
$questionbankclasscolumns = [];
$corequestionbankcolumns = [
/**
* Init required columns.
*
* @return void
*/
protected function init_required_columns(): void {
// Override core columns.
$this->corequestionbankcolumns = [
'add_action_column',
'checkbox_column',
'question_type_column',
'question_name_text_column',
'preview_action_column'
];
}
/**
* Get class for each question bank columns.
*
* @return array
*/
protected function get_class_for_columns(): array {
$questionbankclasscolumns = [];
if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) {
$corequestionbankcolumns[] = 'question_text_row';
$this->corequestionbankcolumns[] = 'question_text_row';
}
foreach ($corequestionbankcolumns as $fullname) {
foreach ($this->corequestionbankcolumns as $fullname) {
$shortname = $fullname;
if (class_exists('mod_quiz\\question\\bank\\' . $fullname)) {
$fullname = 'mod_quiz\\question\\bank\\' . $fullname;
@ -88,40 +124,18 @@ class custom_view extends \core_question\local\bank\view {
$questionbankclasscolumns[$shortname] = '';
}
}
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
foreach ($plugins as $componentname => $plugin) {
$pluginentrypointobject = new $plugin();
$plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
// Don't need the plugins without column objects.
if (empty($plugincolumnobjects)) {
unset($plugins[$componentname]);
continue;
}
foreach ($plugincolumnobjects as $columnobject) {
$columnname = $columnobject->get_column_name();
foreach ($corequestionbankcolumns as $key => $corequestionbankcolumn) {
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
unset($questionbankclasscolumns[$columnname]);
continue;
}
// Check if it has custom preference selector to view/hide.
if ($columnobject->has_preference() && !$columnobject->get_preference()) {
continue;
}
if ($corequestionbankcolumn === $columnname) {
$questionbankclasscolumns[$columnname] = $columnobject;
}
}
}
}
// Mitigate the error in case of any regression.
foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
if (empty($questionbankclasscolumn)) {
unset($questionbankclasscolumns[$shortname]);
}
}
return $questionbankclasscolumns;
}
/**
* Get the list of qbank plugins with available objects for features.
*
* @return array
*/
protected function get_question_bank_plugins(): array {
// The view is too crowded, exclude some columns.
$questionbankclasscolumns = parent::get_question_bank_plugins();
$questionbankclasscolumns = array_intersect_key($questionbankclasscolumns, array_flip($this->corequestionbankcolumns));
return $questionbankclasscolumns;
}
@ -132,8 +146,8 @@ class custom_view extends \core_question\local\bank\view {
protected function default_sort(): array {
// Using the extended class for quiz specific sort.
return [
'qbank_viewquestiontype\\question_type_column' => 1,
'mod_quiz\\question\\bank\\question_name_text_column' => 1,
'qbank_viewquestiontype__question_type_column' => SORT_ASC,
'mod_quiz__question__bank__question_name_text_column' => SORT_ASC,
];
}
@ -144,23 +158,13 @@ class custom_view extends \core_question\local\bank\view {
*
* @param bool $quizhasattempts whether the quiz has attempts.
*/
public function set_quiz_has_attempts($quizhasattempts): void {
private function set_quiz_has_attempts($quizhasattempts): void {
$this->quizhasattempts = $quizhasattempts;
if ($quizhasattempts && isset($this->visiblecolumns['addtoquizaction'])) {
unset($this->visiblecolumns['addtoquizaction']);
}
}
/**
* Question preview url.
*
* @param \stdClass $question
* @return \moodle_url
*/
public function preview_question_url($question) {
return quiz_question_preview_url($this->quiz, $question);
}
/**
* URL of add to quiz.
*
@ -171,6 +175,7 @@ class custom_view extends \core_question\local\bank\view {
$params = $this->baseurl->params();
$params['addquestion'] = $questionid;
$params['sesskey'] = sesskey();
$params['cmid'] = $this->cm->id;
return new \moodle_url('/mod/quiz/edit.php', $params);
}
@ -186,7 +191,7 @@ class custom_view extends \core_question\local\bank\view {
*/
public function render($pagevars, $tabname): string {
ob_start();
$this->display($pagevars, $tabname);
$this->display();
$out = ob_get_contents();
ob_end_clean();
return $out;
@ -216,8 +221,15 @@ class custom_view extends \core_question\local\bank\view {
echo \html_writer::end_tag('div');
}
/**
* Override the base implementation in \core_question\local\bank\view
* because we don't want to print new question form in the fragment
* for the modal.
*
* @param false|mixed|\stdClass $category
* @param bool $canadd
*/
protected function create_new_question_form($category, $canadd): void {
// Don't display this.
}
/**
@ -228,36 +240,6 @@ class custom_view extends \core_question\local\bank\view {
protected function display_question_bank_header(): void {
}
/**
* Override the base implementation in \core_question\bank\view
* because we don't want it to read from the $_POST global variables
* for the sort parameters since they are not present in a fragment.
*
* Unfortunately the best we can do is to look at the URL for
* those parameters (only marginally better really).
*/
protected function init_sort_from_params(): void {
$this->sort = [];
for ($i = 1; $i <= self::MAX_SORTS; $i++) {
if (!$sort = $this->baseurl->param('qbs' . $i)) {
break;
}
// Work out the appropriate order.
$order = 1;
if ($sort[0] == '-') {
$order = -1;
$sort = substr($sort, 1);
if (!$sort) {
break;
}
}
// Deal with subsorts.
list($colname) = $this->parse_subsort($sort);
$this->get_column_type($colname);
$this->sort[$sort] = $order;
}
}
protected function build_query(): void {
// Get the required tables and fields.
[$fields, $joins] = $this->get_component_requirements(array_merge($this->requiredcolumns, $this->questionactions));
@ -265,7 +247,7 @@ class custom_view extends \core_question\local\bank\view {
// Build the order by clause.
$sorts = [];
foreach ($this->sort as $sortname => $sortorder) {
list($colname, $subsort) = $this->parse_subsort($sortname);
[$colname, $subsort] = $this->parse_subsort($sortname);
$sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
}
@ -275,50 +257,50 @@ class custom_view extends \core_question\local\bank\view {
JOIN {question_bank_entries} be
ON be.id = v.questionbankentryid
WHERE be.id = qbe.id)';
$readyonly = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' ";
$tests = ['q.parent = 0', $latestversion, $readyonly];
$onlyready = '((' . "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "'" .'))';
$this->sqlparams = [];
$conditions = [];
foreach ($this->searchconditions as $searchcondition) {
if ($searchcondition->where()) {
$tests[] = '((' . $searchcondition->where() .'))';
$conditions[] = '((' . $searchcondition->where() .'))';
}
if ($searchcondition->params()) {
$this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
}
}
$majorconditions = ['q.parent = 0', $latestversion, $onlyready];
// Get higher level filter condition.
$jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT;
$nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : '';
$separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';
// Build the SQL.
$sql = ' FROM {question} q ' . implode(' ', $joins);
$sql .= ' WHERE ' . implode(' AND ', $tests);
$sql .= ' WHERE ' . implode(' AND ', $majorconditions);
if (!empty($conditions)) {
$sql .= ' AND ' . $nonecondition . ' ( ';
$sql .= implode($separator, $conditions);
$sql .= ' ) ';
}
$this->countsql = 'SELECT count(1)' . $sql;
$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
}
public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
global $CFG;
list(, $contextid) = explode(',', $cat);
$catcontext = \context::instance_by_id($contextid);
$thiscontext = $this->get_most_specific_context();
// Category selection form.
$this->display_question_bank_header();
// Display tag filter if usetags setting is enabled/enablefilters is true.
if ($this->enablefilters) {
if (is_array($this->customfilterobjects)) {
foreach ($this->customfilterobjects as $filterobjects) {
$this->searchconditions[] = $filterobjects;
public function add_standard_search_conditions(): void {
foreach ($this->plugins as $componentname => $plugin) {
if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
$pluginentrypointobject = new $plugin();
if ($componentname === 'qbank_managecategories') {
$pluginentrypointobject = new quiz_managecategories_feature();
}
} else {
if ($CFG->usetags) {
array_unshift($this->searchconditions,
new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
if ($componentname === 'qbank_viewquestiontext' || $componentname === 'qbank_deletequestion') {
continue;
}
$pluginobjects = $pluginentrypointobject->get_question_filters($this);
foreach ($pluginobjects as $pluginobject) {
$this->add_searchcondition($pluginobject, $pluginobject->get_condition_key());
}
array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
array_unshift($this->searchconditions, new custom_category_condition(
$cat, $recurse, $editcontexts, $this->baseurl, $this->course));
}
}
$this->display_options_form($showquestiontext);
}
/**

View File

@ -16,6 +16,8 @@
namespace mod_quiz\question\bank\filter;
use qbank_managecategories\helper;
/**
* A custom filter condition for quiz to select question categories.
*
@ -27,19 +29,23 @@ namespace mod_quiz\question\bank\filter;
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_category_condition extends \core_question\bank\search\category_condition {
class custom_category_condition extends \qbank_managecategories\category_condition {
public function display_options() {
global $PAGE;
$displaydata = [];
$catmenu = custom_category_condition_helper::question_category_options($this->contexts, true, 0,
true, -1, false);
$displaydata['categoryselect'] = \html_writer::select($catmenu, 'category', $this->cat, [],
['class' => 'searchoptions custom-select', 'id' => 'id_selectacategory']);
$displaydata['categorydesc'] = '';
if ($this->category) {
$displaydata['categorydesc'] = $this->print_category_info($this->category);
public function get_initial_values() {
$catmenu = custom_category_condition_helper::question_category_options($this->contexts, true, 0, true, -1, false);
$values = [];
foreach ($catmenu as $menu) {
foreach ($menu as $catlist) {
foreach ($catlist as $key => $value) {
$values[] = (object) [
// Remove contextid from value.
'value' => strpos($key, ',') === false ? $key : substr($key, 0, strpos($key, ',')),
'title' => $value,
'selected' => ($key === $this->cat),
];
}
}
}
return $PAGE->get_renderer('core_question', 'bank')->render_category_condition($displaydata);
return $values;
}
}

View File

@ -18,6 +18,8 @@ namespace mod_quiz\question\bank;
use core_question\local\bank\question_version_status;
use core_question\local\bank\random_question_loader;
use core_question\question_reference_manager;
use qbank_tagquestion\tag_condition;
use qubaid_condition;
defined('MOODLE_INTERNAL') || die();
@ -110,6 +112,7 @@ class qbank_helper {
slot.displaynumber,
slot.requireprevious,
qsr.filtercondition,
qsr.usingcontextid,
qv.status,
qv.id AS versionid,
qv.version,
@ -172,11 +175,12 @@ class qbank_helper {
if ($slot->filtercondition) {
// Unpack the information about a random question.
$filtercondition = json_decode($slot->filtercondition);
$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.
$slot->category = $filtercondition->questioncategoryid;
$slot->randomrecurse = (bool) $filtercondition->includingsubcategories;
$slot->randomtags = isset($filtercondition->tags) ? (array) $filtercondition->tags : [];
$filter = json_decode($slot->filtercondition, true);
$slot->filtercondition = question_reference_manager::convert_legacy_set_reference_filter_condition($filter);
$slot->category = $slot->filtercondition['filter']['category']['values'][0] ?? 0;
$slot->qtype = 'random';
$slot->name = get_string('random', 'quiz');
$slot->length = 1;
@ -208,9 +212,12 @@ class qbank_helper {
*/
public static function get_tag_ids_for_slot(\stdClass $slotdata): array {
$tagids = [];
foreach ($slotdata->randomtags as $taginfo) {
[$id] = explode(',', $taginfo, 2);
$tagids[] = $id;
if (!isset($slotdata->filtercondition['filter'])) {
return $tagids;
}
$filter = $slotdata->filtercondition['filter'];
if (isset($filter['qtagids'])) {
$tagids = $filter['qtagids']['values'];
}
return $tagids;
}
@ -222,10 +229,19 @@ class qbank_helper {
* @return string that can be used to display the random slot.
*/
public static function describe_random_question(\stdClass $slotdata): string {
global $DB;
$category = $DB->get_record('question_categories', ['id' => $slotdata->category]);
return \question_bank::get_qtype('random')->question_name(
$category, $slotdata->randomrecurse, $slotdata->randomtags);
$qtagids = self::get_tag_ids_for_slot($slotdata);
if ($qtagids) {
$tagnames = [];
$tags = \core_tag_tag::get_bulk($qtagids, 'id, name');
foreach ($tags as $tag) {
$tagnames[] = $tag->name;
}
$description = get_string('randomqnametags', 'mod_quiz', implode(",", $tagnames));
} else {
$description = get_string('randomqname', 'mod_quiz');
}
return shorten_text($description, 255);
}
/**
@ -249,8 +265,9 @@ class qbank_helper {
// Random question.
$randomloader = new random_question_loader($qubaids, []);
$newqusetionid = $randomloader->get_next_question_id($slotdata->category,
$slotdata->randomrecurse, self::get_tag_ids_for_slot($slotdata));
$fitlercondition = $slotdata->filtercondition;
$filter = $fitlercondition['filter'] ?? [];
$newqusetionid = $randomloader->get_next_filtered_question_id($filter);
if ($newqusetionid === null) {
throw new \moodle_exception('notenoughrandomquestions', 'quiz');

View File

@ -14,12 +14,26 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
namespace mod_quiz\question\bank;
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
use core_question\local\bank\view;
use mod_quiz\question\bank\filter\custom_category_condition;
/**
* Class quiz_managecategories_feature
*
* Overrides the default categories feature with a custom category condition.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT EU Ltd.
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_managecategories_feature extends \qbank_managecategories\plugin_feature {
public function get_question_filters(view $qbank = null): array {
return [
new custom_category_condition($qbank),
];
}
}

View File

@ -0,0 +1,48 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_quiz\question\bank;
/**
* Subclass to customise the view of the question bank for the quiz editing screen.
*
* @package mod_quiz
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author 2022 Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class random_question_view extends custom_view {
/**
* Init required columns.
*
* @return void
*/
protected function init_required_columns(): void {
// Override core columns.
$this->corequestionbankcolumns = [
'question_type_column',
'question_name_text_column',
'preview_action_column'
];
}
/**
* Prints the table of questions in a category with interactions
*
* @param \context $catcontext*/
protected function display_bottom_controls(\context $catcontext): void {
}
}

View File

@ -570,12 +570,14 @@ class quiz_settings {
continue;
}
if ($questiondata->qtype === 'random' && $includepotential) {
if (!isset($qcategories[$questiondata->category])) {
$qcategories[$questiondata->category] = false;
}
if (!empty($questiondata->filtercondition)) {
$filtercondition = json_decode($questiondata->filtercondition);
$qcategories[$questiondata->category] = !empty($filtercondition->includingsubcategories);
$filtercondition = $questiondata->filtercondition;
if (!empty($filtercondition)) {
$filter = $filtercondition['filter'];
if (isset($filter['category'])) {
foreach ($filter['category']['values'] as $catid) {
$qcategories[$catid] = $filter['category']['filteroptions']['includesubcategories'];
}
}
}
} else {
if (!in_array($questiondata->qtype, $questiontypes)) {

View File

@ -1389,4 +1389,43 @@ class structure {
// All the associated code for this method have been removed to get rid of accidental call or errors.
return [];
}
/**
* Add a random question to the quiz at a given point.
*
* @param int $addonpage the page on which to add the question.
* @param int $number the number of random questions to add.
* @param array $filtercondition the filter condition. Must contain at least a category filter.
*/
public function add_random_questions(int $addonpage, int $number, array $filtercondition): void {
global $DB;
if (!isset($filtercondition['filter']['category'])) {
throw new \invalid_parameter_exception('$filtercondition must contain at least a category filter.');
}
$categoryid = $filtercondition['filter']['category']['values'][0];
$category = $DB->get_record('question_categories', ['id' => $categoryid]);
if (!$category) {
new \moodle_exception('invalidcategoryid');
}
$catcontext = \context::instance_by_id($category->contextid);
require_capability('moodle/question:useall', $catcontext);
// Create the selected number of random questions.
for ($i = 0; $i < $number; $i++) {
// Slot data.
$randomslotdata = new stdClass();
$randomslotdata->quizid = $this->get_quizid();
$randomslotdata->usingcontextid = context_module::instance($this->get_cmid())->id;
$randomslotdata->questionscontextid = $category->contextid;
$randomslotdata->maxmark = 1;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_quiz($this->get_quiz());
$randomslot->set_filter_condition(json_encode($filtercondition));
$randomslot->insert($addonpage);
}
}
}

View File

@ -215,4 +215,20 @@ $functions = [
'capabilities' => 'mod/quiz:reopenattempts',
'ajax' => true,
],
'mod_quiz_add_random_questions' => [
'classname' => 'mod_quiz\external\add_random_questions',
'description' => 'Add a number of random questions to a quiz.',
'type' => 'write',
'capabilities' => 'mod/quiz:manage',
'ajax' => true,
],
'mod_quiz_update_filter_condition' => [
'classname' => 'mod_quiz\external\update_filter_condition',
'description' => 'Update filter condition for a random question slot.',
'type' => 'write',
'capabilities' => 'mod/quiz:manage',
'ajax' => true,
],
];

View File

@ -49,7 +49,7 @@ require_once($CFG->dirroot . '/question/editlib.php');
$mdlscrollto = optional_param('mdlscrollto', '', PARAM_INT);
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
question_edit_setup('editq', '/mod/quiz/edit.php', true);
question_edit_setup('editq', '/mod/quiz/edit.php', true);
$PAGE->set_url($thispageurl);
$PAGE->set_secondary_active_tab("mod_quiz_edit");
@ -166,10 +166,6 @@ $event = \mod_quiz\event\edit_page_viewed::create([
]);
$event->trigger();
// Get the question bank view.
$questionbank = new mod_quiz\question\bank\custom_view($contexts, $thispageurl, $course, $cm, $quiz);
$questionbank->set_quiz_has_attempts($quizhasattempts);
// End of process commands =====================================================.
$PAGE->set_pagelayout('incourse');
@ -184,8 +180,13 @@ $node = $PAGE->settingsnav->find('mod_quiz_edit', navigation_node::TYPE_SETTING)
if ($node) {
$node->make_active();
}
echo $OUTPUT->header();
// Add random question - result message.
if ($message = optional_param('message', '', PARAM_TEXT)) {
core\notification::add($message, core\notification::SUCCESS);
}
echo $OUTPUT->header();
// Initialise the JavaScript.
$quizeditconfig = new stdClass();
$quizeditconfig->url = $thispageurl->out(true, ['qbanktool' => '0']);

View File

@ -24,9 +24,11 @@
*/
use mod_quiz\quiz_settings;
use mod_quiz\question\bank\random_question_view;
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/lib.php');
$slotid = required_param('slotid', PARAM_INT);
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
@ -53,16 +55,14 @@ $PAGE->add_body_class('limitedwidth');
$setreference = $DB->get_record('question_set_references',
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
$filterconditions = json_decode($setreference->filtercondition);
$filterconditions = json_decode($setreference->filtercondition, true);
// Validate the question category.
if (!$category = $DB->get_record('question_categories', ['id' => $filterconditions->questioncategoryid])) {
new moodle_exception('categorydoesnotexist', 'question', $returnurl);
}
$params = $filterconditions;
$params['cmid'] = $cm->id;
$extraparams['view'] = random_question_view::class;
// Check permissions.
$catcontext = context::instance_by_id($category->contextid);
require_capability('moodle/question:useall', $catcontext);
// Build required parameters.
[$contexts, $thispageurl, $cm, $pagevars, $extraparams] = build_required_parameters_for_custom_view($params, $extraparams);
$thiscontext = $quizobj->get_context();
$contexts = new core_question\local\bank\question_edit_contexts($thiscontext);
@ -72,11 +72,15 @@ $mform = new mod_quiz\form\randomquestion_form(new moodle_url('/mod/quiz/editran
// Set the form data.
$toform = new stdClass();
$toform->category = "{$category->id},{$category->contextid}";
$toform->includesubcategories = $filterconditions->includingsubcategories;
$toform->category = $filterconditions['filter']['category']['values'][0];
$includesubcategories = false;
if (!empty($filterconditions['filter']['category']['filteroptions']['includesubcategories'])) {
$includesubcategories = true;
}
$toform->includesubcategories = $includesubcategories;
$toform->fromtags = [];
if (isset($filterconditions->tags)) {
$currentslottags = $filterconditions->tags;
if (isset($filterconditions['tags'])) {
$currentslottags = $filterconditions['tags'];
foreach ($currentslottags as $slottag) {
$toform->fromtags[] = $slottag;
}
@ -130,11 +134,22 @@ $PAGE->set_title('Random question');
$PAGE->set_heading($COURSE->fullname);
$PAGE->navbar->add('Random question');
// Custom View.
$questionbank = new random_question_view($contexts, $thispageurl, $course, $cm, $params, $extraparams);
// Output.
$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
$data = new \stdClass();
$data->questionbank = $renderer->question_bank_contents($questionbank, $params);
$data->cmid = $cm->id;
$data->slotid = $slot->id;
$data->returnurl = $returnurl;
$updateform = $OUTPUT->render_from_template('mod_quiz/update_filter_condition_form', $data);
$PAGE->requires->js_call_amd('mod_quiz/update_random_question_filter_condition', 'init');
// Display a heading, question editing form.
echo $OUTPUT->header();
$heading = get_string('randomediting', 'mod_quiz');
echo $OUTPUT->heading_with_help($heading, 'randomquestion', 'mod_quiz');
$mform->display();
echo $updateform;
echo $OUTPUT->footer();

View File

@ -32,6 +32,7 @@ $string['addaquestion'] = 'a new question';
$string['addasection'] = 'a new section heading';
$string['addarandomquestion'] = 'a random question';
$string['addarandomquestion_help'] = 'When a random question is added, it results in a randomly-chosen question from the category being inserted into the quiz. This means that different students are likely to get a different selection of questions, and when a quiz allows multiple attempts then each attempt is likely to contain a new selection of questions.';
$string['addarandomquestion_success'] = 'Random questions have been added';
$string['addarandomselectedquestion'] = 'Add a random selected question ...';
$string['adddescriptionlabel'] = 'Add a description item';
$string['addingquestion'] = 'Adding a question';
@ -819,6 +820,8 @@ $string['randomfromexistingcategory'] = 'Random question from an existing catego
$string['randomfromunavailabletag'] = '{$a} (unavailable)';
$string['randomnumber'] = 'Number of random questions';
$string['randomnosubcat'] = 'Questions from this category only, not its subcategories.';
$string['randomqname'] = 'Random question based on filter condition';
$string['randomqnametags'] = 'Random question based on filter condition with tags: {$a}';
$string['randomquestion'] = 'Random question';
$string['randomquestion_help'] = 'A random question is a way of inserting a randomly-chosen question from a specified category or by a specified tag into an activity.';
$string['randomquestiontags'] = 'Tags';
@ -1051,6 +1054,8 @@ $string['unfinished'] = 'open';
$string['ungraded'] = 'Ungraded';
$string['unit'] = 'Unit';
$string['unknowntype'] = 'Question type not supported at line {$a}. The question will be ignored';
$string['updatefilterconditon'] = 'Update filter conditions';
$string['updatefilterconditon_success'] = 'Successfully updated filter conditions';
$string['updateoverdueattemptstask'] = 'Updating overdue quiz attempts';
$string['updatesettings'] = 'Update quiz settings';
$string['updatequizslotswithrandomxofy'] = 'Updating quiz slots with "random" question data ({$a->done}/{$a->total})';

View File

@ -26,10 +26,11 @@
*/
use qbank_managecategories\helper;
defined('MOODLE_INTERNAL') || die();
use mod_quiz\access_manager;
use mod_quiz\form\add_random_form;
use mod_quiz\grade_calculator;
use mod_quiz\question\bank\custom_view;
use mod_quiz\question\bank\qbank_helper;
@ -41,6 +42,7 @@ use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
require_once($CFG->dirroot . '/calendar/lib.php');
require_once($CFG->dirroot . '/question/editlib.php');
/**#@+
* Option controlling what options are offered on the quiz settings form.
@ -2392,27 +2394,27 @@ function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event,
* @param array $args The fragment arguments.
* @return string The rendered mform fragment.
*/
function mod_quiz_output_fragment_quiz_question_bank($args) {
global $CFG, $DB, $PAGE;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/question/editlib.php');
function mod_quiz_output_fragment_quiz_question_bank($args): string {
global $PAGE;
$querystring = preg_replace('/^\?/', '', $args['querystring']);
// Retrieve params.
$params = [];
$extraparams = [];
$querystring = parse_url($args['querystring'], PHP_URL_QUERY);
parse_str($querystring, $params);
// Build the required resources. The $params are all cleaned as
// part of this process.
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
question_build_edit_resources('editq', '/mod/quiz/edit.php', $params, custom_view::DEFAULT_PAGE_SIZE);
$viewclass = \mod_quiz\question\bank\custom_view::class;
$extraparams['view'] = $viewclass;
// Get the course object and related bits.
$course = get_course($quiz->course);
// Build required parameters.
[$contexts, $thispageurl, $cm, $pagevars, $extraparams] =
build_required_parameters_for_custom_view($params, $extraparams);
$course = get_course($cm->course);
require_capability('mod/quiz:manage', $contexts->lowest());
// Create quiz question bank view.
$questionbank = new custom_view($contexts, $thispageurl, $course, $cm, $quiz);
$questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id));
// Custom View.
$questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $pagevars, $extraparams);
// Output.
$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
@ -2433,32 +2435,54 @@ function mod_quiz_output_fragment_quiz_question_bank($args) {
* @return string The rendered mform fragment.
*/
function mod_quiz_output_fragment_add_random_question_form($args) {
global $CFG;
global $PAGE, $OUTPUT;
$contexts = new \core_question\local\bank\question_edit_contexts($args['context']);
$formoptions = [
'contexts' => $contexts,
'cat' => $args['cat']
];
$formdata = [
'category' => $args['cat'],
'addonpage' => $args['addonpage'],
'returnurl' => $args['returnurl'],
'cmid' => $args['cmid']
$extraparams = [];
// Build required parameters.
[$contexts, $thispageurl, $cm, $pagevars, $extraparams] =
build_required_parameters_for_custom_view($args, $extraparams);
// Additional param to differentiate with other question bank view.
$extraparams['view'] = mod_quiz\question\bank\random_question_view::class;
$course = get_course($cm->course);
require_capability('mod/quiz:manage', $contexts->lowest());
// Custom View.
$questionbank = new mod_quiz\question\bank\random_question_view($contexts, $thispageurl, $course, $cm, $pagevars, $extraparams);
$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
$questionbankoutput = $renderer->question_bank_contents($questionbank, $pagevars);
$maxrand = 100;
for ($i = 1; $i <= min(100, $maxrand); $i++) {
$randomcount[] = ['value' => $i, 'name' => $i];
}
// Parent category select.
$usablecontexts = $contexts->having_cap('moodle/question:useall');
$categoriesarray = helper::question_category_options($usablecontexts);
$catoptions = [];
foreach ($categoriesarray as $group => $opts) {
// Options for each category group.
$categories = [];
foreach ($opts as $context => $name) {
$categories[] = ['value' => $context, 'name' => $name];
}
$catoptions[] = ['label' => $group, 'options' => $categories];
}
// Template data.
$data = [
'questionbank' => $questionbankoutput,
'randomoptions' => $randomcount,
'questioncategoryoptions' => $catoptions,
];
$form = new add_random_form(
new \moodle_url('/mod/quiz/addrandom.php'),
$formoptions,
'post',
'',
null,
true,
$formdata
);
$form->set_data($formdata);
$result = $OUTPUT->render_from_template('mod_quiz/add_random_question_form', $data);
return $form->render();
return $result;
}
/**
@ -2505,6 +2529,61 @@ function quiz_delete_references($quizid): void {
}
}
/**
* Question data fragment to get the question html via ajax call.
*
* @param array $args
* @return string
*/
function mod_quiz_output_fragment_question_data(array $args): string {
// Return if there is no args.
if (empty($args)) {
return '';
}
// Retrieve params from query string.
[$params, $extraparams] = \core_question\local\bank\filter_condition_manager::extract_parameters_from_fragment_args($args);
// Build required parameters.
$cmid = clean_param($args['cmid'], PARAM_INT);
$thispageurl = new \moodle_url('/mod/quiz/edit.php', ['cmid' => $cmid]);
$thiscontext = \context_module::instance($cmid);
$contexts = new \core_question\local\bank\question_edit_contexts($thiscontext);
$course = get_course($params['courseid']);
[, $cm] = get_module_from_cmid($cmid);
// Custom question bank View.
$viewclass = clean_param($args['view'], PARAM_NOTAGS);
$questionbank = new $viewclass($contexts, $thispageurl, $course, $cm, $params, $extraparams);
// Question table.
return $questionbank->display_questions_table();
}
/**
* Build required parameters for question bank custom view
*
* @param array $params the page parameters
* @param array $extraparams additional parameters
* @return array
*/
function build_required_parameters_for_custom_view(array $params, array $extraparams): array {
// Retrieve questions per page.
$viewclass = $extraparams['view'] ?? null;
$defaultpagesize = $viewclass ? $viewclass::DEFAULT_PAGE_SIZE : DEFAULT_QUESTIONS_PER_PAGE;
// Build the required params.
[$thispageurl, $contexts, $cmid, $cm, , $pagevars] = question_build_edit_resources(
'editq',
'/mod/quiz/edit.php',
array_merge($params, $extraparams),
$defaultpagesize);
// Add cmid so we can retrieve later in extra params.
$extraparams['cmid'] = $cmid;
return [$contexts, $thispageurl, $cm, $pagevars, $extraparams];
}
/**
* Implement the calculate_question_stats callback.
*

View File

@ -213,8 +213,9 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
// Deal with fixed random choices for testing.
if (isset($questionids[$quba->next_slot_number()])) {
if ($randomloader->is_question_available($questiondata->category,
(bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()], $tagids)) {
$filtercondition = $questiondata->filtercondition;
$filters = $filtercondition['filter'] ?? [];
if ($randomloader->is_filtered_question_available($filters, $questionids[$quba->next_slot_number()])) {
$questions[$slot] = question_bank::load_question(
$questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers);
continue;
@ -224,8 +225,10 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
}
// Normal case, pick one at random.
$questionid = $randomloader->get_next_question_id($questiondata->category,
$questiondata->randomrecurse, $tagids);
$filtercondition = $questiondata->filtercondition;
$filters = $filtercondition['filter'] ?? [];
$questionid = $randomloader->get_next_filtered_question_id($filters);
if ($questionid === null) {
throw new moodle_exception('notenoughrandomquestions', 'quiz',
$quizobj->view_url(), $questiondata);
@ -1884,54 +1887,18 @@ function quiz_update_section_firstslots($quizid, $direction, $afterslot, $before
* @param int $addonpage the page on which to add the question.
* @param int $categoryid the question category to add the question from.
* @param int $number the number of random questions to add.
* @param bool $includesubcategories whether to include questoins from subcategories.
* @param int[] $tagids Array of tagids. The question that will be picked randomly should be tagged with all these tags.
* @deprecated Since Moodle 4.3 MDL-72321
* @todo Final deprecation in Moodle 4.7 MDL-78091
*/
function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
$includesubcategories, $tagids = []) {
global $DB;
function quiz_add_random_questions(stdClass $quiz, int $addonpage, int $categoryid, int $number): void {
debugging(
'quiz_add_random_questions is deprecated. Please use mod_quiz\structure::add_random_questions() instead.',
DEBUG_DEVELOPER
);
$category = $DB->get_record('question_categories', ['id' => $categoryid]);
if (!$category) {
new moodle_exception('invalidcategoryid');
}
$catcontext = context::instance_by_id($category->contextid);
require_capability('moodle/question:useall', $catcontext);
// Tags for filter condition.
$tags = \core_tag_tag::get_bulk($tagids, 'id, name');
$tagstrings = [];
foreach ($tags as $tag) {
$tagstrings[] = "{$tag->id},{$tag->name}";
}
// Create the selected number of random questions.
for ($i = 0; $i < $number; $i++) {
// Set the filter conditions.
$filtercondition = new stdClass();
$filtercondition->questioncategoryid = $categoryid;
$filtercondition->includingsubcategories = $includesubcategories ? 1 : 0;
if (!empty($tagstrings)) {
$filtercondition->tags = $tagstrings;
}
if (!isset($quiz->cmid)) {
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
$quiz->cmid = $cm->id;
}
// Slot data.
$randomslotdata = new stdClass();
$randomslotdata->quizid = $quiz->id;
$randomslotdata->usingcontextid = context_module::instance($quiz->cmid)->id;
$randomslotdata->questionscontextid = $category->contextid;
$randomslotdata->maxmark = 1;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_quiz($quiz);
$randomslot->set_filter_condition($filtercondition);
$randomslot->insert($addonpage);
}
$settings = quiz_settings::create($quiz->id);
$structure = \mod_quiz\structure::create_for_quiz($settings);
$structure->add_random_questions($addonpage, $number, $categoryid);
}
/**

View File

@ -197,7 +197,6 @@ Feature: Regrading quiz attempts using the Grades report
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I click on "Add" "link"
And I follow "a random question"
And I set the field "Number of random questions" to "1"
And I press "Add random question"
And I am on the "Quiz for testing regrading" "quiz activity" page logged in as student3
And I click on "Attempt quiz" "button"

View File

@ -0,0 +1,90 @@
{{!
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 mod_quiz/add_random_question_form
From to add random questions.
Example context (json):
{
"returnurl": "http://example.com",
"randomoptions": [
{"name": "1", "value": "1"},
{"name": "2", "value": "2"}
],
"questioncategoryoptions": {
"label": "Category 1",
"options": [
{"name": "Category 2", "value": "1"},
{"name": "Category 3", "value": "2"}
]
},
"questionbank": "<pre>Question bank HTML goes here</pre>"
}
}}
<div class="mt-2 position-relative" data-region="add_random_question_form-container">
<form action="{{returnurl}}" method="POST" id="add_random_question_form">
<fieldset id="id_existingcategoryheader">
<legend>{{#str}} randomfromexistingcategory, mod_quiz {{/str}}</legend>
<!-- Question bank -->
{{{questionbank}}}
<!-- Select number of random question -->
<label>
{{#str}}randomnumber, mod_quiz{{/str}}
<select id="menurandomcount" name="randomcount" class="form-control custom-select">
{{#randomoptions}}
<option value="{{value}}">{{name}}</option>
{{/randomoptions}}
</select>
</label>
<!-- Buttons -->
<div class="modal-footer mt-1" data-region="footer">
<input type="hidden" name="message" value="">
<input type="submit" class="btn btn-primary" name="addrandom" value="{{#str}} addrandomquestion, mod_quiz {{/str}}">
<input type="submit" class="btn btn-secondary" name="cancel" value="{{#str}} cancel {{/str}}">
</div>
</fieldset>
<fieldset id="id_newcategoryheader">
<legend>{{#str}} randomquestionusinganewcategory, mod_quiz {{/str}}</legend>
<!-- New categoryname -->
<label class="mr-md-2 mb-md-0" for="categoryname">{{#str}} name {{/str}} </label>
<input type="text" class="form-control" id="categoryname" name="categoryname" maxlength="254" size="50">
<!-- Select number of random question -->
<label class="mr-md-2 mb-md-0" for="parentcategory">{{#str}} parentcategory, question {{/str}} </label>
<select id="parentcategory" name="parentcategory" class="form-control">
{{#questioncategoryoptions}}
<optgroup label="{{label}}">
{{#options}}
<option value="{{value}}">{{{name}}}</option>
{{/options}}
</optgroup>
{{/questioncategoryoptions}}
</select>
<!-- Buttons -->
<div class="modal-footer mt-1" data-region="footer">
<input type="submit" class="btn btn-primary" name="newcategory" value="{{#str}} createcategoryandaddrandomquestion, mod_quiz {{/str}}">
<input type="submit" class="btn btn-secondary" name="cancel" value="{{#str}} cancel {{/str}}">
</div>
</fieldset>
</form>
</div>

View File

@ -15,26 +15,14 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template mod_quiz/random_question_form_preview
@template mod_quiz/random_question_container
A preview section for the list of questions that match the random
question criteria in the random question form.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* title A cleaned string (use clean_text()) to display.
* body HTML content for the boday
Container for list of filtered questions.
Example context (json):
{}
{
}
}}
<div class="mt-2 position-relative" data-region="random-question-preview-container">
<div data-region="question-count-container"></div>
<div data-region="question-list-container"></div>
{{> core/overlay_loading}}
<div class="categoryquestionscontainer" id="questionscontainer">
</div>

View File

@ -0,0 +1,43 @@
{{!
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 mod_quiz/update_filter_condition_form
From to add random questions.
Example context (json):
{
"questionbank": "<pre>Question bank HTML goes here</pre>",
"returnurl": "https://example.com",
"cmid": "1234",
"slotid": "5678"
}
}}
<div>
<!-- Question bank -->
<div id="questionbank_container">
{{{questionbank}}}
</div>
<!-- submit buttons -->
<form action="{{returnurl}}" method="POST" id="update_filter_condition_form" data-cmid="{{cmid}}" data-slotid="{{slotid}}">
<input type="hidden" name="message" value="">
<input type="submit" class="btn btn-primary" name="update" value="{{#str}} updatefilterconditon, mod_quiz {{/str}}">
<input type="submit" class="btn btn-secondary" name="cancel" value="{{#str}} cancel {{/str}}">
</form>
</div>

View File

@ -23,6 +23,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Quiz attempt walk through using data from csv file.
@ -35,6 +36,8 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
*/
class attempt_walkthrough_from_csv_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* @var string[] names of the files which contain the test data.
*/
@ -134,7 +137,7 @@ class attempt_walkthrough_from_csv_test extends \advanced_testcase {
if ($slotquestion['type'] !== 'random') {
quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
} else {
quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0);
$this->add_random_questions($this->quiz->id, 0, $slotquestion['catid'], 1);
$this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
}
}

View File

@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Quiz attempt walk through.
@ -37,6 +38,8 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
*/
class attempt_walkthrough_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Create a quiz with questions and walk through a quiz attempt.
*/
@ -288,7 +291,7 @@ class attempt_walkthrough_test extends \advanced_testcase {
$numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
// Add random question to the quiz.
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
// Make another category.
$cat2 = $questiongenerator->create_question_category();

View File

@ -276,7 +276,18 @@ class behat_mod_quiz extends behat_question_base {
} else {
$includingsubcategories = clean_param($questiondata['includingsubcategories'], PARAM_BOOL);
}
quiz_add_random_questions($quiz, $page, $question->category, 1, $includingsubcategories);
$filter = [
'category' => [
'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
'values' => [$question->category],
'filteroptions' => ['includesubcategories' => $includingsubcategories],
],
];
$filtercondition['filter'] = $filter;
$settings = quiz_settings::create($quiz->id);
$structure = \mod_quiz\structure::create_for_quiz($settings);
$structure->add_random_questions($page, 1, $filtercondition);
} else {
// Add the question.
quiz_add_quiz_question($question->id, $quiz, $page, $maxmark);

View File

@ -117,7 +117,6 @@ Feature: Edit quiz page - adding things
And I select "Questions" from the "Question bank tertiary navigation" singleselect
And I should see "Question bank"
And I should see "Select a category"
# Create the Essay 01 question.
When I press "Create a new question ..."
@ -131,8 +130,6 @@ Feature: Edit quiz page - adding things
And I should see "Essay 01"
# Create the Essay 02 question.
And I should see "Select a category"
And I set the field "Select a category:" to "Subcat 1"
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
And I click on "Add" "button" in the "Choose a question type to add" "dialogue"
@ -144,7 +141,6 @@ Feature: Edit quiz page - adding things
And I should see "Essay 02"
# Create the Essay 03 question.
And I set the field "Select a category" to "Default for C1"
And I wait until the page is ready
When I press "Create a new question ..."
And I set the field "item_qtype_essay" to "1"
@ -201,8 +197,6 @@ Feature: Edit quiz page - adding things
# Add Esay 02 from question bank.
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I should see "Select a category"
And I set the field "Select a category" to "Subcat 1"
And I click on "Add to quiz" "link" in the "Essay 02" "table_row"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"

View File

@ -40,8 +40,7 @@ Feature: Adding questions to a quiz from the question bank
Then I should see "foo" in the "question 01 name" "table_row"
And I should see "bar" in the "question 02 name" "table_row"
And I should see "qidnum" in the "question 02 name" "table_row"
And I set the field "Filter by tags..." to "foo"
And I press the enter key
When I apply question bank filter "Tag" with value "foo"
And I should see "question 01 name" in the "categoryquestions" "table"
And I should not see "question 02 name" in the "categoryquestions" "table"
@ -76,9 +75,8 @@ Feature: Adding questions to a quiz from the question bank
Then I should see "question 21 name" in the "categoryquestions" "table"
And I should see "question 22 name" in the "categoryquestions" "table"
And I should not see "question 01 name" in the "categoryquestions" "table"
And I click on "Show all 22" "link" in the ".question-showall-text" "css_element"
And I click on "1" "link" in the ".pagination" "css_element"
And I should see "question 01 name" in the "categoryquestions" "table"
And I should see "question 22 name" in the "categoryquestions" "table"
Scenario: Questions are added in the right place with multiple sections
Given the following "questions" exist:

View File

@ -43,7 +43,9 @@ Feature: Adding random questions to a quiz based on category and tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I open the autocomplete suggestions list
And I add question bank filter "Tag"
And I click on "Tag" "field"
And I press the down key
Then "foo" "autocomplete_suggestions" should exist
And "bar" "autocomplete_suggestions" should exist
@ -51,21 +53,16 @@ Feature: Adding random questions to a quiz based on category and tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Category" to "Top for Course 1"
And I wait until the page is ready
And I open the autocomplete suggestions list
And I click on "foo" item in the autocomplete list
Then I should see "question 1 name"
And I should see "question 3 name"
And I should not see "question 2 name"
And I should not see "question 4 name"
And I set the field "Category" to "Questions Category 1"
And I apply question bank filter "Category" with value "Questions Category 1"
And I apply question bank filter "Tag" with value "foo"
And I click on "Apply filters" "button"
And I wait until the page is ready
And I should see "question 1 name"
And I should not see "question 3 name"
And I should not see "question 2 name"
And I should not see "question 4 name"
And I click on "Include questions from subcategories too" "checkbox"
And I set the field "Also show questions from subcategories" to "1"
And I click on "Apply filters" "button"
And I wait until the page is ready
And I should see "question 1 name"
And I should see "question 3 name"
@ -76,10 +73,11 @@ Feature: Adding random questions to a quiz based on category and tags
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Tags" to "foo"
And I apply question bank filter "Tag" with value "foo"
And I select "1" from the "randomcount" singleselect
And I press "Add random question"
And I should see "Random (Questions Category 1, tags: foo)" on quiz page "1"
And I click on "(See questions)" "link"
And I should see "Random question based on filter condition with tags: foo" on quiz page "1"
When I click on "Configure question" "link" in the "Random question based on filter condition with tags: foo" "list_item"
Then I should see "Questions Category 1"
And I should see "foo"
And I should see "question 1 name"
@ -101,8 +99,8 @@ Feature: Adding random questions to a quiz based on category and tags
And I follow "New category"
And I set the following fields to these values:
| Name | New Random category |
| Parent category | Top for Quiz 1 |
| Parent category | Default for Quiz 1 |
And I press "Create category and add random question"
And I should see "Random (New Random category)" on quiz page "1"
And I click on "(See questions)" "link"
Then I should see "Top for Quiz 1"
And I should see "Random question based on filter condition" on quiz page "1"
And I click on "Configure question" "link" in the "Random question based on filter condition" "list_item"
Then I should see "New Random category"

View File

@ -38,18 +38,17 @@ Feature: Editing random questions already in a quiz based on category and tags
And I follow "a random question"
# To actually reproduce MDL-68733 it would be better to set tags easy,essay here, and then below just delete one tag.
# However, the state of Behat for autocomplete fields does not let us actually do that.
And I set the field "Tags" to "easy"
And I apply question bank filter "Tag" with value "easy"
And I press "Add random question"
And I open the "Page 1" add to quiz menu
And I follow "a random question"
And I set the field "Tags" to "hard"
And I apply question bank filter "Tag" with value "hard"
And I press "Add random question"
And I follow "Add page break"
When I click on "Configure question" "link" in the "Random (Questions Category 1, tags: easy)" "list_item"
And I click on "easy" "autocomplete_selection"
And I set the field "Tags" to "essay"
And I press "Save changes"
Then I should see "Random (Questions Category 1, tags: essay)" on quiz page "1"
And I should see "Random (Questions Category 1, tags: hard)" on quiz page "2"
And I click on "Configure question" "link" in the "Questions Category 1, tags: hard" "list_item"
When I click on "Configure question" "link" in the "Random question based on filter condition with tags: easy" "list_item"
And I apply question bank filter "Tag" with value "essay"
And I press "Update filter conditions"
Then I should see "Random question based on filter condition with tags: essay" on quiz page "1"
And I should see "Random question based on filter condition with tags: hard" on quiz page "2"
And I click on "Configure question" "link" in the "Random question based on filter condition with tags: hard" "list_item"
And "hard" "autocomplete_selection" should be visible

View File

@ -261,11 +261,9 @@ Feature: Edit quiz page - remove multiple questions
And I click on "selectquestion-2" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
# To make sure question is deleted completely.
And I reload the page
Then I should see "Random (Test questions)" on quiz page "1"
And I should not see "Random (Test questions)" on quiz page "2"
And I should not see "Random (Test questions)" on quiz page "3"
Then I should see "Random question based on filter condition" on quiz page "1"
And I should not see "Random question based on filter condition" on quiz page "2"
And I should not see "Random question based on filter condition" on quiz page "3"
And I should see "Total of marks: 1.00"
And I should see "Questions: 1"
@ -284,16 +282,15 @@ Feature: Edit quiz page - remove multiple questions
# Delete all questions in page. Page contains multiple questions.
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Number of random questions" to "3"
And I click on "menurandomcount" "select"
And I click on "3" "option"
And I press "Add random question"
And I click on "Select multiple items" "button"
And I press "Select all"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
# To make sure question is deleted completely.
And I reload the page
Then I should not see "Random (Test questions)" on quiz page "1"
And I should not see "Random (Test questions)" on quiz page "2"
And I should not see "Random (Test questions)" on quiz page "3"
Then I should not see "Random question based on filter condition" on quiz page "1"
And I should not see "Random question based on filter condition" on quiz page "2"
And I should not see "Random question based on filter condition" on quiz page "3"
And I should see "Total of marks: 0.00"
And I should see "Questions: 0"

View File

@ -33,23 +33,24 @@ Feature: Moving a question to another category should not affect random question
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Category" to "Used category"
And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
And I should see "Random (Used category)" on quiz page "1"
And I click on "(See questions)" "link"
And I should see "Random question based on filter condition" on quiz page "1"
And I click on "Configure question" "link" in the "Random question based on filter condition" "list_item"
And I should see "Used category"
And I am on "Course 1" course homepage
And I navigate to "Question bank" in current page administration
And I apply question bank filter "Category" with value "Used category"
And I click on "Test question to be moved" "checkbox" in the "Test question to be moved" "table_row"
And I click on "With selected" "button"
And I click on question bulk action "move"
And I set the field "Question category" to "Subcategory"
And I press "Move to"
Then I should see "Test question to be moved"
And the field "Select a category" matches value "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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)"
And I should see "Subcategory (1)"
And I am on the "Quiz 1" "mod_quiz > Edit" page
And I should see "Random (Used category)" on quiz page "1"
And I click on "(See questions)" "link"
And I should see "Random question based on filter condition" on quiz page "1"
And I click on "Configure question" "link" in the "Random question based on filter condition" "list_item"
And I should see "Used category"
@javascript
@ -57,9 +58,9 @@ Feature: Moving a question to another category should not affect random question
Given I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1"
When I open the "last" add to quiz menu
And I follow "a random question"
And I set the field "Category" to "Used category"
And I apply question bank filter "Category" with value "Used category"
And I press "Add random question"
And I should see "Random (Used category)" on quiz page "1"
And I should see "Random question based on filter condition" on quiz page "1"
And I am on the "Course 1" "core_question > course question categories" page
And I click on "Edit this category" "link" in the "Used category" "list_item"
And I set the following fields to these values:
@ -69,4 +70,4 @@ Feature: Moving a question to another category should not affect random question
Then I should see "Used category new"
And I should see "I was edited" in the "Used category new" "list_item"
And I am on the "Quiz 1" "mod_quiz > Edit" page
And I should see "Random (Used category new)" on quiz page "1"
And I should see "Random question based on filter condition" on quiz page "1"

View File

@ -41,6 +41,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Silly class to access mod_quiz_external internal methods.
@ -86,6 +87,8 @@ class testable_mod_quiz_external extends mod_quiz_external {
*/
class external_test extends externallib_advanced_testcase {
use \quiz_question_helper_test_trait;
/** @var \stdClass course record. */
protected $course;
@ -1898,7 +1901,7 @@ class external_test extends externallib_advanced_testcase {
$question = $questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$question = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
$quizobj = quiz_settings::create($quiz->id, $this->student->id);
@ -2024,8 +2027,8 @@ class external_test extends externallib_advanced_testcase {
$question = $questiongenerator->create_question('essay', null, ['category' => $anothercat->id]);
// Add a couple of random questions from the same category.
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
$this->setUser($this->student);
@ -2039,7 +2042,7 @@ class external_test extends externallib_advanced_testcase {
// Add more questions to the quiz, this time from the other category.
$this->setAdminUser();
quiz_add_random_questions($quiz, 0, $anothercat->id, 1, false);
$this->add_random_questions($quiz->id, 0, $anothercat->id, 1);
$this->setUser($this->student);
$result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);

View File

@ -31,12 +31,15 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/lib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
class lib_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
public function test_quiz_has_grades() {
$quiz = new \stdClass();
$quiz->grade = '100.0000';
@ -109,7 +112,7 @@ class lib_test extends \advanced_testcase {
$standardq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_quiz_question($standardq->id, $quiz);
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
// Get the random question.
$randomq = $DB->get_record('question', ['qtype' => 'random']);

View File

@ -16,8 +16,14 @@
namespace mod_quiz;
defined('MOODLE_INTERNAL') || die();
use mod_quiz\question\bank\qbank_helper;
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Class mod_quiz_local_structure_slot_random_test
* Class for tests related to the {@link \mod_quiz\local\structure\slot_random} class.
@ -26,8 +32,12 @@ use mod_quiz\question\bank\qbank_helper;
* @category test
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_quiz\local\structure\slot_random
*/
class local_structure_slot_random_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Constructor test.
*/
@ -57,8 +67,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->questioncategoryid = $category->id;
$filtercondition->includingsubcategories = 1;
$filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], true);
// Slot data.
$randomslotdata = new \stdClass();
@ -69,7 +78,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Insert the random question to the quiz.
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition($filtercondition);
$randomslot->set_filter_condition(json_encode($filtercondition));
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
@ -77,8 +86,8 @@ class local_structure_slot_random_test extends \advanced_testcase {
$record = json_decode($rcp->getValue($randomslot));
$this->assertEquals($quiz->id, $randomslot->get_quiz()->id);
$this->assertEquals($category->id, $record->questioncategoryid);
$this->assertEquals(1, $record->includingsubcategories);
$this->assertEquals($category->id, $record->filters->category->values[0]);
$this->assertTrue($record->filters->category->filteroptions->includesubcategories);
$rcp = $rc->getProperty('record');
$rcp->setAccessible(true);
@ -100,12 +109,11 @@ class local_structure_slot_random_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
quiz_add_random_questions($quiz, 0, $category->id, 1, false);
$this->add_random_questions($quiz->id, 0, $category->id, 1);
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->questioncategoryid = $category->id;
$filtercondition->includingsubcategories = 1;
$filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], 1);
// Slot data.
$randomslotdata = new \stdClass();
@ -115,7 +123,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition($filtercondition);
$randomslot->set_filter_condition(json_encode($filtercondition));
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
unset($quiz->cmid);
@ -137,12 +145,11 @@ class local_structure_slot_random_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
quiz_add_random_questions($quiz, 0, $category->id, 1, false);
$this->add_random_questions($quiz->id, 0, $category->id, 1);
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->questioncategoryid = $category->id;
$filtercondition->includingsubcategories = 1;
$filtercondition->filters = \question_filter_test_helper::create_filters([$category->id], 1);
// Slot data.
$randomslotdata = new \stdClass();
@ -152,7 +159,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
$randomslotdata->questionscontextid = $category->contextid;
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_filter_condition($filtercondition);
$randomslot->set_filter_condition(json_encode($filtercondition));
// The create_instance had injected an additional cmid propery to the quiz. Let's remove that.
unset($quiz->cmid);
@ -178,7 +185,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
quiz_add_random_questions($quiz, 0, $category->id, 1, false);
$this->add_random_questions($quiz->id, 0, $category->id, 1);
// Slot data.
$randomslotdata = new \stdClass();
@ -203,147 +210,24 @@ class local_structure_slot_random_test extends \advanced_testcase {
return [$randomslot, $tags];
}
public function test_set_tags() {
public function test_set_tags_filter() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar']);
$qtagids = [$tags['foo']->id, $tags['bar']->id];
$filtercondition = new \stdClass();
$randomslot->set_tags([$tags['foo'], $tags['bar']]);
$randomslot->set_filter_condition($filtercondition);
$filtercondition->filters = \question_filter_test_helper::create_filters([], 0, $qtagids);
$randomslot->set_filter_condition(json_encode($filtercondition));
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
$this->assertEquals([
$tags['foo']->id => $tags['foo'],
$tags['bar']->id => $tags['bar'],
], (array)json_decode($tagspropery)->tags);
}
public function test_set_tags_twice() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
// Set tags for the first time.
$filtercondition = new \stdClass();
$randomslot->set_tags([$tags['foo'], $tags['bar']]);
// Now set the tags again.
$randomslot->set_tags([$tags['baz']]);
$randomslot->set_filter_condition($filtercondition);
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
$this->assertEquals([
$tags['baz']->id => $tags['baz'],
], (array)json_decode($tagspropery)->tags);
}
public function test_set_tags_duplicates() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
$filtercondition = new \stdClass();
$randomslot->set_tags([$tags['foo'], $tags['bar'], $tags['foo']]);
$randomslot->set_filter_condition($filtercondition);
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('filtercondition');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
$this->assertEquals([
$tags['foo']->id => $tags['foo'],
$tags['bar']->id => $tags['bar'],
], (array)json_decode($tagspropery)->tags);
}
public function test_set_tags_by_id() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
$filtercondition = new \stdClass();
$randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id]);
$randomslot->set_filter_condition($filtercondition);
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('tags');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
// The set_tags_by_id function only retrieves id and name fields of the tag object.
$this->assertCount(2, $tagspropery);
$this->assertArrayHasKey($tags['foo']->id, $tagspropery);
$this->assertArrayHasKey($tags['bar']->id, $tagspropery);
$this->assertEquals(
(object)['id' => $tags['foo']->id, 'name' => $tags['foo']->name],
$tagspropery[$tags['foo']->id]->to_object()
);
$this->assertEquals(
(object)['id' => $tags['bar']->id, 'name' => $tags['bar']->name],
$tagspropery[$tags['bar']->id]->to_object()
);
}
public function test_set_tags_by_id_twice() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
// Set tags for the first time.
$randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id]);
// Now set the tags again.
$randomslot->set_tags_by_id([$tags['baz']->id]);
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('tags');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
// The set_tags_by_id function only retrieves id and name fields of the tag object.
$this->assertCount(1, $tagspropery);
$this->assertArrayHasKey($tags['baz']->id, $tagspropery);
$this->assertEquals(
(object)['id' => $tags['baz']->id, 'name' => $tags['baz']->name],
$tagspropery[$tags['baz']->id]->to_object()
);
}
public function test_set_tags_by_id_duplicates() {
$this->resetAfterTest();
$this->setAdminUser();
list($randomslot, $tags) = $this->setup_for_test_tags(['foo', 'bar', 'baz']);
$randomslot->set_tags_by_id([$tags['foo']->id, $tags['bar']->id], $tags['foo']->id);
$rc = new \ReflectionClass('\mod_quiz\local\structure\slot_random');
$rcp = $rc->getProperty('tags');
$rcp->setAccessible(true);
$tagspropery = $rcp->getValue($randomslot);
// The set_tags_by_id function only retrieves id and name fields of the tag object.
$this->assertCount(2, $tagspropery);
$this->assertArrayHasKey($tags['foo']->id, $tagspropery);
$this->assertArrayHasKey($tags['bar']->id, $tagspropery);
$this->assertEquals(
(object)['id' => $tags['foo']->id, 'name' => $tags['foo']->name],
$tagspropery[$tags['foo']->id]->to_object()
);
$this->assertEquals(
(object)['id' => $tags['bar']->id, 'name' => $tags['bar']->name],
$tagspropery[$tags['bar']->id]->to_object()
);
$this->assertEquals([$tags['foo']->id, $tags['bar']->id],
(array)json_decode($tagspropery)->filters->qtagids->values);
}
public function test_insert() {
@ -389,8 +273,7 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Set the filter conditions.
$filtercondition = new \stdClass();
$filtercondition->questioncategoryid = $category->id;
$filtercondition->includingsubcategories = 1;
$filtercondition->filter = \question_filter_test_helper::create_filters([$category->id], true, [$footag->id, $bartag->id]);
// Slot data.
$randomslotdata = new \stdClass();
@ -401,26 +284,26 @@ class local_structure_slot_random_test extends \advanced_testcase {
// Insert the random question to the quiz.
$randomslot = new \mod_quiz\local\structure\slot_random($randomslotdata);
$randomslot->set_tags([$footag, $bartag]);
$randomslot->set_filter_condition($filtercondition);
$randomslot->set_filter_condition(json_encode($filtercondition));
$randomslot->insert(1); // Put the question on the first page of the quiz.
$slots = qbank_helper::get_question_structure($quiz->id, $quizcontext);
$quizslot = reset($slots);
$this->assertEquals($category->id, $quizslot->category);
$this->assertEquals(1, $quizslot->randomrecurse);
$this->assertEquals(1, $quizslot->maxmark);
$tagspropery = $quizslot->randomtags;
$filter = $quizslot->filtercondition['filter'];
$this->assertCount(2, $tagspropery);
$this->assertEquals($category->id, $filter['category']['values'][0]);
$this->assertTrue($filter['category']['filteroptions']['includesubcategories']);
$this->assertEquals(1, $quizslot->maxmark);
$this->assertCount(2, $filter['qtagids']['values']);
$this->assertEqualsCanonicalizing(
[
['tagid' => $footag->id, 'tagname' => $footag->name],
['tagid' => $bartag->id, 'tagname' => $bartag->name]
['tagid' => $footag->id],
['tagid' => $bartag->id]
],
array_map(function($slottag) {
return ['tagid' => $slottag->id, 'tagname' => $slottag->name];
}, $tagspropery));
array_map(function($tagid) {
return ['tagid' => $tagid];
}, $filter['qtagids']['values']));
}
}

View File

@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Unit tests for (some of) mod/quiz/locallib.php.
@ -41,6 +41,8 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
*/
class locallib_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
public function test_quiz_rescale_grade() {
$quiz = new \stdClass();
$quiz->decimalpoints = 2;
@ -523,7 +525,7 @@ class locallib_test extends \advanced_testcase {
$tagids[] = $tagobjects[$tagname]->id;
}
}
quiz_add_random_questions($quiz, 0, $cat->id, 1, false, $tagids);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
}
return [$quiz, $tagobjects];

View File

@ -49,7 +49,8 @@ class quiz_question_bank_view_test extends \advanced_testcase {
// Create a question in the default category.
$contexts = new question_edit_contexts($context);
$cat = question_make_default_categories($contexts->all());
question_make_default_categories($contexts->all());
$cat = question_get_default_category($context->id);
$questiondata = $questiongenerator->create_question('numerical', null,
['name' => 'Example question', 'category' => $cat->id]);
@ -58,17 +59,19 @@ class quiz_question_bank_view_test extends \advanced_testcase {
$cache->delete($questiondata->id);
// Generate the view.
$view = new custom_view($contexts, new \moodle_url('/'), $course, $cm, $quiz);
ob_start();
$pagevars = [
$params = [
'qpage' => 0,
'qperpage' => 20,
'cat' => $cat->id . ',' . $context->id,
'recurse' => false,
'showhidden' => false,
'qbshowtext' => false
'qbshowtext' => false,
'tabname' => 'editq'
];
$view->display($pagevars, 'editq');
$extraparams = ['cmid' => $cm->id];
$view = new custom_view($contexts, new \moodle_url('/'), $course, $cm, $params, $extraparams);
ob_start();
$view->display();
$html = ob_get_clean();
// Verify the output includes the expected question.

View File

@ -92,7 +92,7 @@ trait quiz_question_helper_test_trait {
$cat = $questiongenerator->create_question_category($override);
$questiongenerator->create_question('truefalse', null, ['category' => $cat->id]);
$questiongenerator->create_question('essay', null, ['category' => $cat->id]);
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
}
/**
@ -180,4 +180,28 @@ trait quiz_question_helper_test_trait {
protected function duplicate_quiz($course, $quiz): ?\cm_info {
return duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
}
/**
* Add random questions to a quiz, with a filter condition based on a category ID.
*
* @param int $quizid The quiz to add the questions to.
* @param int $page The page number to add the questions to.
* @param int $categoryid The category ID to use for the filter condition.
* @param int $number The number of questions to add.
* @return void
*/
protected function add_random_questions(int $quizid, int $page, int $categoryid, int $number): void {
$settings = quiz_settings::create($quizid);
$structure = \mod_quiz\structure::create_for_quiz($settings);
$filtercondition = [
'filter' => [
'category' => [
'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
'values' => [$categoryid],
'filteroptions' => ['includesubcategories' => false],
],
],
];
$structure->add_random_questions($page, $number, $filtercondition);
}
}

View File

@ -470,7 +470,7 @@ class quiz_question_restore_test extends \advanced_testcase {
quiz_add_quiz_question($saq->id, $quiz, 1, 3);
quiz_add_quiz_question($numq->id, $quiz, 2, 2);
quiz_add_quiz_question($matchq->id, $quiz, 3, 1);
quiz_add_random_questions($quiz, 3, $randomcat->id, 2, false);
$this->add_random_questions($quiz->id, 3, $randomcat->id, 2);
$quizobj = quiz_settings::create($quiz->id, $user1->id);
$originalstructure = \mod_quiz\structure::create_for_quiz($quizobj);

View File

@ -16,6 +16,11 @@
namespace mod_quiz;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Unit tests for quiz events.
*
@ -26,6 +31,8 @@ namespace mod_quiz;
*/
class structure_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Create a course with an empty quiz.
* @return array with three elements quiz, cm and course.
@ -691,7 +698,7 @@ class structure_test extends \advanced_testcase {
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
quiz_add_random_questions($quizobj->get_quiz(), 1, $cat->id, 1, false);
$this->add_random_questions($quizobj->get_quizid(), 1, $cat->id, 1);
$structure = structure::create_for_quiz($quizobj);
$sql = 'SELECT qsr.*
FROM {question_set_references} qsr

View File

@ -16,9 +16,14 @@
namespace qbank_managecategories;
defined('MOODLE_INTERNAL') || die();
use moodle_url;
use core_question\local\bank\question_edit_contexts;
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Unit tests for helper class.
*
@ -30,6 +35,8 @@ use core_question\local\bank\question_edit_contexts;
*/
class helper_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* @var \context_module module context.
*/
@ -97,7 +104,7 @@ class helper_test extends \advanced_testcase {
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);
$this->add_random_questions($this->quiz->id, 0, $qcat2->id, 1);
// 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 qsr.*

View File

@ -16,11 +16,16 @@
namespace qbank_statistics;
defined('MOODLE_INTERNAL') || die();
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
use question_engine;
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Tests for question statistics.
*
@ -31,6 +36,8 @@ use question_engine;
*/
class helper_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Test quizzes that contain a specified question.
*
@ -100,7 +107,7 @@ class helper_test extends \advanced_testcase {
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
// Add a random question to quiz3.
quiz_add_random_questions($quiz3, 0, $cat->id, 1, false);
$this->add_random_questions($quiz3->id, 0, $cat->id, 1);
$this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
// Quiz 3 will now be in one of these arrays.

View File

@ -16,8 +16,13 @@
namespace qbank_usage;
defined('MOODLE_INTERNAL') || die();
use mod_quiz\quiz_attempt;
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Helper test.
*
@ -29,6 +34,8 @@ use mod_quiz\quiz_attempt;
*/
class helper_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* @var \stdClass $quiz
*/
@ -133,7 +140,7 @@ class helper_test extends \advanced_testcase {
$this->setAdminUser();
$cat = $this->questiongenerator->create_question_category();
$question = $this->questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_random_questions($this->quiz, 1, $cat->id, 1, false);
$this->add_random_questions($this->quiz->id, 1, $cat->id, 1);
$qdef = \question_bank::load_question($question->id);
$count = helper::get_question_entry_usage_count($qdef);
@ -226,7 +233,7 @@ class helper_test extends \advanced_testcase {
$this->setAdminUser();
$cat = $this->questiongenerator->create_question_category();
$question = $this->questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
quiz_add_random_questions($this->quiz, 1, $cat->id, 1, false);
$this->add_random_questions($this->quiz->id, 1, $cat->id, 1);
$this->attempt_quiz();

View File

@ -270,7 +270,10 @@ function question_build_edit_resources($edittab, $baseurl, $params,
}
if (!empty($params['filter'])) {
$cleanparams['filter'] = json_decode($params['filter'], true);
if (!is_array($params['filter'])) {
$params['filter'] = json_decode($params['filter'], true);
}
$cleanparams['filter'] = $params['filter'];
}
$cmid = $cleanparams['cmid'];

View File

@ -16,6 +16,8 @@
namespace core_question\local\statistics;
defined('MOODLE_INTERNAL') || die();
use advanced_testcase;
use context;
use context_module;
@ -27,6 +29,9 @@ use mod_quiz\quiz_settings;
use question_engine;
use ReflectionMethod;
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Tests for question statistics.
*
@ -37,6 +42,8 @@ use ReflectionMethod;
*/
class statistics_bulk_loader_test extends advanced_testcase {
use \quiz_question_helper_test_trait;
/** @var float Delta used when comparing statistics values out-of 1. */
protected const DELTA = 0.00005;
@ -111,7 +118,7 @@ class statistics_bulk_loader_test extends advanced_testcase {
$this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
// Add a random question to quiz3.
quiz_add_random_questions($quiz3, 0, $cat->id, 1, false);
$this->add_random_questions($quiz3->id, 0, $cat->id, 1, false);
$this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
// Quiz 3 will now be in one of these arrays.

View File

@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Tests for the {@see \core_question\local\bank\random_question_loader} class.
@ -35,6 +36,8 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
*/
class random_question_loader_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
public function test_empty_category_gives_null() {
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
@ -104,7 +107,7 @@ class random_question_loader_test extends \advanced_testcase {
$cat = $generator->create_question_category();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course]);
quiz_add_random_questions($quiz, 1, $cat->id, 1);
$this->add_random_questions($quiz->id, 1, $cat->id, 1);
$loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]));
$filters = question_filter_test_helper::create_filters([$cat->id]);