Merge branch 'MDL-61135-master' of git://github.com/ryanwyllie/moodle
This commit is contained in:
@@ -159,6 +159,7 @@ $string['fileformat'] = 'File format';
|
||||
$string['filesareacourse'] = 'the course files area';
|
||||
$string['filesareasite'] = 'the site files area';
|
||||
$string['filestomove'] = 'Move / copy files to {$a}?';
|
||||
$string['filterbytags'] = 'Filter by tags...';
|
||||
$string['firsttry'] = 'First try';
|
||||
$string['flagged'] = 'Flagged';
|
||||
$string['flagthisquestion'] = 'Flag this question';
|
||||
@@ -230,6 +231,7 @@ $string['nopermissionmove'] = 'You don\'t have permission to move questions from
|
||||
$string['noprobs'] = 'No problems found in your question database.';
|
||||
$string['noquestions'] = 'No questions were found that could be exported. Make sure that you have selected a category to export that contains questions.';
|
||||
$string['noquestionsinfile'] = 'There are no questions in the import file';
|
||||
$string['notagfiltersapplied'] = 'No tag filters applied';
|
||||
$string['notenoughanswers'] = 'This type of question requires at least {$a} answers';
|
||||
$string['notenoughdatatoeditaquestion'] = 'Neither a question id, nor a category id and question type, was specified.';
|
||||
$string['notenoughdatatomovequestions'] = 'You need to provide the question ids of questions you want to move.';
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -272,7 +272,12 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
|
||||
if (value.state() == 'pending') {
|
||||
// We're still waiting for the body promise to resolve so
|
||||
// let's show a loading icon.
|
||||
body.animate({height: '100px'}, 150);
|
||||
var height = body.innerHeight();
|
||||
if (height < 100) {
|
||||
height = 100;
|
||||
}
|
||||
|
||||
body.animate({height: height + 'px'}, 150);
|
||||
|
||||
body.html('');
|
||||
contentPromise = Templates.render(TEMPLATES.LOADING, {})
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<span class="loading-icon">{{#pix}} y/loading, core, {{#str}} loading {{/str}} {{/pix}}</span>
|
||||
<span class="loading-icon">{{#pix}} i/loading, core, {{#str}} loading {{/str}} {{/pix}}</span>
|
||||
|
||||
@@ -33,6 +33,6 @@
|
||||
Example context (json):
|
||||
{}
|
||||
}}
|
||||
<span class="overlay-icon-container hidden" data-region="overlay-icon-container">
|
||||
<span class="overlay-icon-container {{$hiddenclass}}{{^visible}}hidden{{/visible}}{{/hiddenclass}}" data-region="overlay-icon-container">
|
||||
{{> core/loading }}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
define(["jquery","core/yui","core/notification","core/modal","core/modal_events","core/modal_registry","core/fragment"],function(a,b,c,d,e,f,g){var h=!1,i={ADD_TO_QUIZ_CONTAINER:"td.addtoquizaction",ANCHOR:"a[href]",PREVIEW_CONTAINER:"td.previewaction",SEARCH_OPTIONS:"#advancedsearch",DISPLAY_OPTIONS:"#displayoptions"},j=function(a){d.call(this,a),this.contextId=null,this.addOnPageId=null};return j.TYPE="mod_quiz-quiz-question-bank",j.prototype=Object.create(d.prototype),j.prototype.constructor=j,j.prototype.setContextId=function(a){this.contextId=a},j.prototype.getContextId=function(){return this.contextId},j.prototype.setAddOnPageId=function(a){this.addOnPageId=a},j.prototype.getAddOnPageId=function(){return this.addOnPageId},j.prototype.show=function(){return this.reloadBodyContent(window.location.search),d.prototype.show.call(this)},j.prototype.reloadBodyContent=function(a){var b=g.loadFragment("mod_quiz","quiz_question_bank",this.getContextId(),{querystring:a}).fail(c.exception);this.setBody(b)},j.prototype.handleAddToQuizEvent=function(a,b){var c=b.attr("href")+"&addonpage="+this.getAddOnPageId();b.attr("href",c)},j.prototype.handlePreviewContainerEvent=function(a,b){var c=["height=600","width=800","top=0","left=0","menubar=0","location=0","scrollbars","resizable","toolbar","status","directories=0","fullscreen=0","dependent"];window.openpopup(a,{url:b.attr("href"),name:"questionpreview",options:c.join(",")})},j.prototype.handleDisplayOptionFormEvent=function(b){b.stopPropagation(),b.preventDefault();var c=a(b.target).closest(i.DISPLAY_OPTIONS),d="?"+c.serialize();this.reloadBodyContent(d)},j.prototype.registerDisplayOptionListeners=function(){this.getModal().on("change",i.DISPLAY_OPTIONS,function(b){var c=a(b.target);c.attr("aria-autocomplete")||this.handleDisplayOptionFormEvent(b)}.bind(this)),this.getModal().on("submit",i.DISPLAY_OPTIONS,function(a){this.handleDisplayOptionFormEvent(a)}.bind(this))},j.prototype.registerEventListeners=function(){d.prototype.registerEventListeners.call(this),this.registerDisplayOptionListeners(),this.getModal().on("click",i.ANCHOR,function(b){var c=a(b.currentTarget);return c.closest(i.ADD_TO_QUIZ_CONTAINER).length?void this.handleAddToQuizEvent(b,c):c.closest(i.PREVIEW_CONTAINER).length?void this.handlePreviewContainerEvent(b,c):void(c.closest(i.SEARCH_OPTIONS).length||(b.preventDefault(),this.reloadBodyContent(c.prop("search"))))}.bind(this)),this.getRoot().on(e.bodyRendered,function(){b.use("moodle-core-formchangechecker",function(){M.core_formchangechecker.reset_form_dirty_state()})})},h||(f.register(j.TYPE,j,"core/modal"),h=!0),j});
|
||||
+1
@@ -0,0 +1 @@
|
||||
define(["jquery","core/notification","core/custom_interaction_events","core/modal_factory","mod_quiz/modal_quiz_question_bank"],function(a,b,c,d,e){var f={ADD_QUESTION_LINKS:'.menu [data-action="questionbank"]'};return{init:function(g){var h=a("body");d.create({type:e.TYPE,large:!0},[h,f.ADD_QUESTION_LINKS]).then(function(b){return b.setContextId(g),h.on(c.events.activate,f.ADD_QUESTION_LINKS,function(c){var d=a(c.target).closest(f.ADD_QUESTION_LINKS);b.setAddOnPageId(d.attr("data-addonpage")),b.setTitle(d.attr("data-header"))}),b}).fail(b.exception)}}});
|
||||
@@ -0,0 +1,311 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Contain the logic for the question bank modal.
|
||||
*
|
||||
* @module mod_quiz/modal_quiz_question_bank
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define([
|
||||
'jquery',
|
||||
'core/yui',
|
||||
'core/notification',
|
||||
'core/modal',
|
||||
'core/modal_events',
|
||||
'core/modal_registry',
|
||||
'core/fragment'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Y,
|
||||
Notification,
|
||||
Modal,
|
||||
ModalEvents,
|
||||
ModalRegistry,
|
||||
Fragment
|
||||
) {
|
||||
|
||||
var registered = false;
|
||||
var SELECTORS = {
|
||||
ADD_TO_QUIZ_CONTAINER: 'td.addtoquizaction',
|
||||
ANCHOR: 'a[href]',
|
||||
PREVIEW_CONTAINER: 'td.previewaction',
|
||||
SEARCH_OPTIONS: '#advancedsearch',
|
||||
DISPLAY_OPTIONS: '#displayoptions',
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for the Modal.
|
||||
*
|
||||
* @param {object} root The root jQuery element for the modal
|
||||
*/
|
||||
var ModalQuizQuestionBank = function(root) {
|
||||
Modal.call(this, root);
|
||||
|
||||
this.contextId = null;
|
||||
this.addOnPageId = null;
|
||||
};
|
||||
|
||||
ModalQuizQuestionBank.TYPE = 'mod_quiz-quiz-question-bank';
|
||||
ModalQuizQuestionBank.prototype = Object.create(Modal.prototype);
|
||||
ModalQuizQuestionBank.prototype.constructor = ModalQuizQuestionBank;
|
||||
|
||||
/**
|
||||
* Save the Moodle context id that the question bank is being
|
||||
* rendered in.
|
||||
*
|
||||
* @method setContextId
|
||||
* @param {int} id
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.setContextId = function(id) {
|
||||
this.contextId = id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the saved Moodle context id.
|
||||
*
|
||||
* @method getContextId
|
||||
* @return {int}
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.getContextId = function() {
|
||||
return this.contextId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the id of the page that the question should be added to
|
||||
* when the user clicks the add to quiz link.
|
||||
*
|
||||
* @method setAddOnPageId
|
||||
* @param {int} id
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.setAddOnPageId = function(id) {
|
||||
this.addOnPageId = id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the saved page id for the question to be added it.
|
||||
*
|
||||
* @method getAddOnPageId
|
||||
* @return {int}
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.getAddOnPageId = function() {
|
||||
return this.addOnPageId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the parent show function.
|
||||
*
|
||||
* Reload the body contents when the modal is shown. The current
|
||||
* window URL is used to inform the new content that should be
|
||||
* displayed.
|
||||
*
|
||||
* @method show
|
||||
* @return {void}
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.show = function() {
|
||||
this.reloadBodyContent(window.location.search);
|
||||
return Modal.prototype.show.call(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces the current body contents with a new version of the question
|
||||
* bank.
|
||||
*
|
||||
* The contents of the question bank are generated using the provided
|
||||
* query string.
|
||||
*
|
||||
* @method reloadBodyContent
|
||||
* @param {string} queryString URL encoded string.
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.reloadBodyContent = function(queryString) {
|
||||
// Load the question bank fragment to be displayed in the modal.
|
||||
var promise = Fragment.loadFragment(
|
||||
'mod_quiz',
|
||||
'quiz_question_bank',
|
||||
this.getContextId(),
|
||||
{
|
||||
querystring: queryString
|
||||
}
|
||||
).fail(Notification.exception);
|
||||
|
||||
this.setBody(promise);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the URL of the anchor element that the user clicked on to make
|
||||
* sure that the question is added to the correct page.
|
||||
*
|
||||
* @method handleAddToQuizEvent
|
||||
* @param {event} e A JavaScript event
|
||||
* @param {object} anchorElement The anchor element that was triggered
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.handleAddToQuizEvent = function(e, anchorElement) {
|
||||
// If the user clicks the plus icon to add the question to the page
|
||||
// directly then we need to intercept the click in order to adjust the
|
||||
// href and include the correct add on page id before the page is
|
||||
// redirected.
|
||||
var href = anchorElement.attr('href') + '&addonpage=' + this.getAddOnPageId();
|
||||
anchorElement.attr('href', href);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a popup window to show the preview of the question.
|
||||
*
|
||||
* @method handlePreviewContainerEvent
|
||||
* @param {event} e A JavaScript event
|
||||
* @param {object} anchorElement The anchor element that was triggered
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.handlePreviewContainerEvent = function(e, anchorElement) {
|
||||
var popupOptions = [
|
||||
'height=600',
|
||||
'width=800',
|
||||
'top=0',
|
||||
'left=0',
|
||||
'menubar=0',
|
||||
'location=0',
|
||||
'scrollbars',
|
||||
'resizable',
|
||||
'toolbar',
|
||||
'status',
|
||||
'directories=0',
|
||||
'fullscreen=0',
|
||||
'dependent'
|
||||
];
|
||||
window.openpopup(e, {
|
||||
url: anchorElement.attr('href'),
|
||||
name: 'questionpreview',
|
||||
options: popupOptions.join(',')
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.handleDisplayOptionFormEvent = function(e) {
|
||||
// Stop propagation to prevent other wild event handlers
|
||||
// from submitting the form on change.
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
var form = $(e.target).closest(SELECTORS.DISPLAY_OPTIONS);
|
||||
var 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
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.registerDisplayOptionListeners = function() {
|
||||
// Listen for changes to the display options form.
|
||||
this.getModal().on('change', SELECTORS.DISPLAY_OPTIONS, function(e) {
|
||||
// Get the element that was changed.
|
||||
var 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);
|
||||
}.bind(this));
|
||||
|
||||
// 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, function(e) {
|
||||
this.handleDisplayOptionFormEvent(e);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up all of the event handling for the modal.
|
||||
*
|
||||
* @method registerEventListeners
|
||||
*/
|
||||
ModalQuizQuestionBank.prototype.registerEventListeners = function() {
|
||||
// Apply parent event listeners.
|
||||
Modal.prototype.registerEventListeners.call(this);
|
||||
|
||||
// Set up the event handlers for all of the display options.
|
||||
this.registerDisplayOptionListeners();
|
||||
|
||||
this.getModal().on('click', SELECTORS.ANCHOR, function(e) {
|
||||
var anchorElement = $(e.currentTarget);
|
||||
|
||||
// If the anchor element was the add to quiz link.
|
||||
if (anchorElement.closest(SELECTORS.ADD_TO_QUIZ_CONTAINER).length) {
|
||||
this.handleAddToQuizEvent(e, anchorElement);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the anchor element was a preview question link.
|
||||
if (anchorElement.closest(SELECTORS.PREVIEW_CONTAINER).length) {
|
||||
this.handlePreviewContainerEvent(e, anchorElement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on expand/collaspse search-options. Has its own handler.
|
||||
// We should not interfere.
|
||||
if (anchorElement.closest(SELECTORS.SEARCH_OPTIONS).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else means reload the pop-up contents.
|
||||
e.preventDefault();
|
||||
this.reloadBodyContent(anchorElement.prop('search'));
|
||||
}.bind(this));
|
||||
|
||||
// Disable the form change checker when the body is rendered.
|
||||
this.getRoot().on(ModalEvents.bodyRendered, function() {
|
||||
// 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.
|
||||
Y.use('moodle-core-formchangechecker', function() {
|
||||
M.core_formchangechecker.reset_form_dirty_state();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Automatically register with the modal registry the first time this module is
|
||||
// imported so that you can create modals of this type using the modal factory.
|
||||
if (!registered) {
|
||||
ModalRegistry.register(
|
||||
ModalQuizQuestionBank.TYPE,
|
||||
ModalQuizQuestionBank,
|
||||
'core/modal'
|
||||
);
|
||||
|
||||
registered = true;
|
||||
}
|
||||
|
||||
return ModalQuizQuestionBank;
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Initialise the question bank modal on the quiz page.
|
||||
*
|
||||
* @module mod_quiz/quizquestionbank
|
||||
* @package mod_quiz
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'core/notification',
|
||||
'core/custom_interaction_events',
|
||||
'core/modal_factory',
|
||||
'mod_quiz/modal_quiz_question_bank'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
Notification,
|
||||
CustomEvents,
|
||||
ModalFactory,
|
||||
ModalQuizQuestionBank
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
ADD_QUESTION_LINKS: '.menu [data-action="questionbank"]',
|
||||
};
|
||||
|
||||
return {
|
||||
init: function(contextId) {
|
||||
var body = $('body');
|
||||
|
||||
// Create a question bank modal using the factory.
|
||||
// The same modal will be used by all of the add question
|
||||
// links on the page. The content of the modal will be
|
||||
// changed depending on which link is clicked.
|
||||
ModalFactory.create(
|
||||
{
|
||||
type: ModalQuizQuestionBank.TYPE,
|
||||
large: true
|
||||
},
|
||||
// Created a deligated listener rather than a single
|
||||
// trigger element.
|
||||
[body, SELECTORS.ADD_QUESTION_LINKS]
|
||||
).then(function(modal) {
|
||||
// Save the Moodle context id that the modal is being rendered in.
|
||||
modal.setContextId(contextId);
|
||||
|
||||
body.on(CustomEvents.events.activate, SELECTORS.ADD_QUESTION_LINKS, function(e) {
|
||||
// We need to listen for activations on the trigger elements because there are
|
||||
// several on the page and we need to know which one was activated in order to
|
||||
// set some relevant data on the modal.
|
||||
var triggerElement = $(e.target).closest(SELECTORS.ADD_QUESTION_LINKS);
|
||||
modal.setAddOnPageId(triggerElement.attr('data-addonpage'));
|
||||
modal.setTitle(triggerElement.attr('data-header'));
|
||||
});
|
||||
|
||||
return modal;
|
||||
}).fail(Notification.exception);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -100,10 +100,9 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
if ($structure->can_be_edited()) {
|
||||
$popups = '';
|
||||
|
||||
$popups .= $this->question_bank_loading();
|
||||
$this->page->requires->yui_module('moodle-mod_quiz-quizquestionbank',
|
||||
'M.mod_quiz.quizquestionbank.init',
|
||||
array('class' => 'questionbank', 'cmid' => $structure->get_cmid()));
|
||||
$this->page->requires->js_call_amd('mod_quiz/quizquestionbank', 'init', [
|
||||
$contexts->lowest()->id
|
||||
]);
|
||||
|
||||
$popups .= $this->random_question_form($pageurl, $contexts, $pagevars);
|
||||
$this->page->requires->yui_module('moodle-mod_quiz-randomquestion',
|
||||
@@ -1250,7 +1249,8 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
public function question_bank_contents(\mod_quiz\question\bank\custom_view $questionbank, array $pagevars) {
|
||||
|
||||
$qbank = $questionbank->render('editq', $pagevars['qpage'], $pagevars['qperpage'],
|
||||
$pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['qbshowtext']);
|
||||
$pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['qbshowtext'],
|
||||
$pagevars['qtagids']);
|
||||
return html_writer::div(html_writer::div($qbank, 'bd'), 'questionbankformforpopup');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,9 +138,10 @@ class custom_view extends \core_question\bank\view {
|
||||
*
|
||||
* @return string HTML code for the form
|
||||
*/
|
||||
public function render($tabname, $page, $perpage, $cat, $recurse, $showhidden, $showquestiontext) {
|
||||
public function render($tabname, $page, $perpage, $cat, $recurse, $showhidden,
|
||||
$showquestiontext, $tagids = []) {
|
||||
ob_start();
|
||||
$this->display($tabname, $page, $perpage, $cat, $recurse, $showhidden, $showquestiontext);
|
||||
$this->display($tabname, $page, $perpage, $cat, $recurse, $showhidden, $showquestiontext, $tagids);
|
||||
$out = ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $out;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Defines the custom question bank view used in the question bank modal.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category question
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_quiz\question\bank;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Subclass to customise the view of the question bank for a fragment.
|
||||
*
|
||||
* This view is to be used when returning the question bank as part of
|
||||
* a fragment.
|
||||
*
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class fragment_view extends custom_view {
|
||||
/**
|
||||
* Override the base implementation in \core_question\bank\view
|
||||
* because we don't want to print the headers in the fragment
|
||||
* for the modal.
|
||||
*/
|
||||
protected function display_question_bank_header() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$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, $subsort) = $this->parse_subsort($sort);
|
||||
$this->requiredcolumns[$colname] = $this->get_column_type($colname);
|
||||
$this->sort[$sort] = $order;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2402,3 +2402,44 @@ function mod_quiz_core_calendar_event_timestart_updated(\calendar_event $event,
|
||||
$event->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the question bank in a fargment output. This allows
|
||||
* the question bank to be displayed in a modal.
|
||||
*
|
||||
* The only expected argument provided in the $args array is
|
||||
* 'querystring'. The value should be the list of parameters
|
||||
* URL encoded and used to build the question bank page.
|
||||
*
|
||||
* The individual list of parameters expected can be found in
|
||||
* question_build_edit_resources.
|
||||
*
|
||||
* @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');
|
||||
|
||||
$querystring = preg_replace('/^\?/', '', $args['querystring']);
|
||||
$params = [];
|
||||
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);
|
||||
|
||||
// Get the course object and related bits.
|
||||
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
|
||||
require_capability('mod/quiz:manage', $contexts->lowest());
|
||||
|
||||
// Create quiz question bank view.
|
||||
$questionbank = new mod_quiz\question\bank\fragment_view($contexts, $thispageurl, $course, $cm, $quiz);
|
||||
$questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id));
|
||||
|
||||
// Output.
|
||||
$renderer = $PAGE->get_renderer('mod_quiz', 'edit');
|
||||
return $renderer->question_bank_contents($questionbank, $pagevars);
|
||||
}
|
||||
|
||||
@@ -1,49 +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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Ajax script to update the contents of the question bank dialogue.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('AJAX_SCRIPT', true);
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
require_once($CFG->dirroot . '/question/editlib.php');
|
||||
|
||||
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
|
||||
question_edit_setup('editq', '/mod/quiz/edit.php', true);
|
||||
|
||||
// Get the course object and related bits.
|
||||
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
|
||||
require_capability('mod/quiz:manage', $contexts->lowest());
|
||||
|
||||
// Create quiz question bank view.
|
||||
$questionbank = new mod_quiz\question\bank\custom_view($contexts, $thispageurl, $course, $cm, $quiz);
|
||||
$questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id));
|
||||
|
||||
// Output.
|
||||
$output = $PAGE->get_renderer('mod_quiz', 'edit');
|
||||
$contents = $output->question_bank_contents($questionbank, $pagevars);
|
||||
echo json_encode(array(
|
||||
'status' => 'OK',
|
||||
'contents' => $contents,
|
||||
));
|
||||
@@ -0,0 +1,49 @@
|
||||
@core @core_question
|
||||
Feature: Adding questions to a quiz from the question bank
|
||||
In order to re-use questions
|
||||
As a teacher
|
||||
I want to add questions from the question bank
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber |
|
||||
| quiz | Quiz 1 | Quiz 1 for testing the Add menu | C1 | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | user | questiontext |
|
||||
| Test questions | essay | question 1 name | admin | Question 1 text |
|
||||
| Test questions | essay | question 2 name | teacher1 | Question 2 text |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Questions" node in "Course administration > Question bank"
|
||||
And I click on "Edit" "link" in the "question 1 name" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Tags | foo |
|
||||
And I press "id_submitbutton"
|
||||
And I click on "Edit" "link" in the "question 2 name" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Tags | bar |
|
||||
And I press "id_submitbutton"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Quiz 1"
|
||||
And I navigate to "Edit quiz" in current page administration
|
||||
And I open the "last" add to quiz menu
|
||||
And I follow "from question bank"
|
||||
|
||||
@javascript
|
||||
Scenario: The questions can be filtered by tag
|
||||
When I set the field "Filter by tags..." to "foo"
|
||||
And I press key "13" in the field "Filter by tags..."
|
||||
Then I should see "question 1 name" in the "categoryquestions" "table"
|
||||
And I should not see "question 2 name" in the "categoryquestions" "table"
|
||||
@@ -1,5 +1,8 @@
|
||||
This files describes API changes in the quiz code.
|
||||
|
||||
=== 3.5 ===
|
||||
* Removed questionbank.ajax.php. Please use the quiz_question_bank fragment instead.
|
||||
|
||||
=== 3.3.2 ===
|
||||
|
||||
* quiz_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function
|
||||
|
||||
Vendored
-231
@@ -1,231 +0,0 @@
|
||||
YUI.add('moodle-mod_quiz-quizquestionbank', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Add questions from question bank functionality for a popup in quiz editing page.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
var CSS = {
|
||||
QBANKLOADING: 'div.questionbankloading',
|
||||
ADDQUESTIONLINKS: '.menu [data-action="questionbank"]',
|
||||
ADDTOQUIZCONTAINER: 'td.addtoquizaction',
|
||||
PREVIEWCONTAINER: 'td.previewaction',
|
||||
SEARCHOPTIONS: '#advancedsearch'
|
||||
};
|
||||
|
||||
var PARAMS = {
|
||||
PAGE: 'addonpage',
|
||||
HEADER: 'header'
|
||||
};
|
||||
|
||||
var POPUP = function() {
|
||||
POPUP.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(POPUP, Y.Base, {
|
||||
loadingDiv: '',
|
||||
dialogue: null,
|
||||
addonpage: 0,
|
||||
searchRegionInitialised: false,
|
||||
|
||||
create_dialogue: function() {
|
||||
// Create a dialogue on the page and hide it.
|
||||
var config = {
|
||||
headerContent: '',
|
||||
bodyContent: Y.one(CSS.QBANKLOADING),
|
||||
draggable: true,
|
||||
modal: true,
|
||||
centered: true,
|
||||
width: null,
|
||||
visible: false,
|
||||
postmethod: 'form',
|
||||
footerContent: null,
|
||||
extraClasses: ['mod_quiz_qbank_dialogue']
|
||||
};
|
||||
this.dialogue = new M.core.dialogue(config);
|
||||
this.dialogue.bodyNode.delegate('click', this.link_clicked, 'a[href]', this);
|
||||
this.dialogue.hide();
|
||||
|
||||
this.loadingDiv = this.dialogue.bodyNode.getHTML();
|
||||
|
||||
Y.later(100, this, function() {
|
||||
this.load_content(window.location.search);
|
||||
});
|
||||
},
|
||||
|
||||
initializer: function() {
|
||||
if (!Y.one(CSS.QBANKLOADING)) {
|
||||
return;
|
||||
}
|
||||
this.create_dialogue();
|
||||
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDQUESTIONLINKS, this);
|
||||
},
|
||||
|
||||
display_dialogue: function(e) {
|
||||
e.preventDefault();
|
||||
this.dialogue.set('headerContent', e.currentTarget.getData(PARAMS.HEADER));
|
||||
|
||||
this.addonpage = e.currentTarget.getData(PARAMS.PAGE);
|
||||
var controlsDiv = this.dialogue.bodyNode.one('.modulespecificbuttonscontainer');
|
||||
if (controlsDiv) {
|
||||
var hidden = controlsDiv.one('input[name=addonpage]');
|
||||
if (!hidden) {
|
||||
hidden = controlsDiv.appendChild('<input type="hidden" name="addonpage">');
|
||||
}
|
||||
hidden.set('value', this.addonpage);
|
||||
}
|
||||
|
||||
this.initialiseSearchRegion();
|
||||
this.dialogue.show();
|
||||
},
|
||||
|
||||
load_content: function(queryString) {
|
||||
Y.log('Starting load.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
this.dialogue.bodyNode.append(this.loadingDiv);
|
||||
|
||||
// If to support old IE.
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState(null, '', M.cfg.wwwroot + '/mod/quiz/edit.php' + queryString);
|
||||
}
|
||||
|
||||
Y.io(M.cfg.wwwroot + '/mod/quiz/questionbank.ajax.php' + queryString, {
|
||||
method: 'GET',
|
||||
on: {
|
||||
success: this.load_done,
|
||||
failure: this.load_failed
|
||||
},
|
||||
context: this
|
||||
});
|
||||
|
||||
Y.log('Load request sent.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
},
|
||||
|
||||
load_done: function(transactionid, response) {
|
||||
var result = JSON.parse(response.responseText);
|
||||
if (!result.status || result.status !== 'OK') {
|
||||
// Because IIS is useless, Moodle can't send proper HTTP response
|
||||
// codes, so we have to detect failures manually.
|
||||
this.load_failed(transactionid, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Y.log('Load completed.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
|
||||
this.dialogue.bodyNode.setHTML(result.contents);
|
||||
Y.use('moodle-question-chooser', function() {
|
||||
M.question.init_chooser({});
|
||||
});
|
||||
this.dialogue.bodyNode.one('form').delegate('change', this.options_changed, '.searchoptions', this);
|
||||
|
||||
if (this.dialogue.visible) {
|
||||
Y.later(0, this.dialogue, this.dialogue.centerDialogue);
|
||||
}
|
||||
M.question.qbankmanager.init();
|
||||
|
||||
this.searchRegionInitialised = false;
|
||||
if (this.dialogue.get('visible')) {
|
||||
this.initialiseSearchRegion();
|
||||
}
|
||||
|
||||
this.dialogue.fire('widget:contentUpdate');
|
||||
// TODO MDL-47602 really, the base class should listen for the even fired
|
||||
// on the previous line, and fix things like makeResponsive.
|
||||
// However, it does not. So the next two lines are a hack to fix up
|
||||
// display issues (e.g. overall scrollbars on the page). Once the base class
|
||||
// is fixed, this comment and the following four lines should be deleted.
|
||||
if (this.dialogue.get('visible')) {
|
||||
this.dialogue.hide();
|
||||
this.dialogue.show();
|
||||
}
|
||||
},
|
||||
|
||||
load_failed: function() {
|
||||
Y.log('Load failed.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
},
|
||||
|
||||
link_clicked: function(e) {
|
||||
// Add question to quiz. mofify the URL, then let it work as normal.
|
||||
if (e.currentTarget.ancestor(CSS.ADDTOQUIZCONTAINER)) {
|
||||
e.currentTarget.set('href', e.currentTarget.get('href') + '&addonpage=' + this.addonpage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Question preview. Needs to open in a pop-up.
|
||||
if (e.currentTarget.ancestor(CSS.PREVIEWCONTAINER)) {
|
||||
window.openpopup(e, {
|
||||
url: e.currentTarget.get('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'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on expand/collaspse search-options. Has its own handler.
|
||||
// We should not interfere.
|
||||
if (e.currentTarget.ancestor(CSS.SEARCHOPTIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else means reload the pop-up contents.
|
||||
e.preventDefault();
|
||||
this.load_content(e.currentTarget.get('search'));
|
||||
},
|
||||
|
||||
options_changed: function(e) {
|
||||
e.preventDefault();
|
||||
this.load_content('?' + Y.IO.stringify(e.currentTarget.get('form')));
|
||||
},
|
||||
|
||||
initialiseSearchRegion: function() {
|
||||
if (this.searchRegionInitialised === true) {
|
||||
return;
|
||||
}
|
||||
if (!Y.one(CSS.SEARCHOPTIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
M.util.init_collapsible_region(Y, "advancedsearch", "question_bank_advanced_search",
|
||||
M.util.get_string('clicktohideshow', 'moodle'));
|
||||
this.searchRegionInitialised = true;
|
||||
}
|
||||
});
|
||||
|
||||
M.mod_quiz = M.mod_quiz || {};
|
||||
M.mod_quiz.quizquestionbank = M.mod_quiz.quizquestionbank || {};
|
||||
M.mod_quiz.quizquestionbank.init = function() {
|
||||
return new POPUP();
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {
|
||||
"requires": [
|
||||
"base",
|
||||
"event",
|
||||
"node",
|
||||
"io",
|
||||
"io-form",
|
||||
"yui-later",
|
||||
"moodle-question-qbankmanager",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
});
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
YUI.add("moodle-mod_quiz-quizquestionbank",function(e,t){var n={QBANKLOADING:"div.questionbankloading",ADDQUESTIONLINKS:'.menu [data-action="questionbank"]',ADDTOQUIZCONTAINER:"td.addtoquizaction",PREVIEWCONTAINER:"td.previewaction",SEARCHOPTIONS:"#advancedsearch"},r={PAGE:"addonpage",HEADER:"header"},i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{loadingDiv:"",dialogue:null,addonpage:0,searchRegionInitialised:!1,create_dialogue:function(){var t={headerContent:"",bodyContent:e.one(n.QBANKLOADING),draggable:!0,modal:!0,centered:!0,width:null,visible:!1,postmethod:"form",footerContent:null,extraClasses:["mod_quiz_qbank_dialogue"]};this.dialogue=new M.core.dialogue(t),this.dialogue.bodyNode.delegate("click",this.link_clicked,"a[href]",this),this.dialogue.hide(),this.loadingDiv=this.dialogue.bodyNode.getHTML(),e.later(100,this,function(){this.load_content(window.location.search)})},initializer:function(){if(!e.one(n.QBANKLOADING))return;this.create_dialogue(),e.one("body").delegate("click",this.display_dialogue,n.ADDQUESTIONLINKS,this)},display_dialogue:function(e){e.preventDefault(),this.dialogue.set("headerContent",e.currentTarget.getData(r.HEADER)),this.addonpage=e.currentTarget.getData(r.PAGE);var t=this.dialogue.bodyNode.one(".modulespecificbuttonscontainer");if(t){var n=t.one("input[name=addonpage]");n||(n=t.appendChild('<input type="hidden" name="addonpage">')),n.set("value",this.addonpage)}this.initialiseSearchRegion(),this.dialogue.show()},load_content:function(t){this.dialogue.bodyNode.append(this.loadingDiv),window.history.replaceState&&window.history.replaceState(null,"",M.cfg.wwwroot+"/mod/quiz/edit.php"+t),e.io(M.cfg.wwwroot+"/mod/quiz/questionbank.ajax.php"+t,{method:"GET",on:{success:this.load_done,failure:this.load_failed},context:this})},load_done:function(t,n){var r=JSON.parse(n.responseText);if(!r.status||r.status!=="OK"){this.load_failed(t,n);return}this.dialogue.bodyNode.setHTML(r.contents),e.use("moodle-question-chooser",function(){M.question.init_chooser({})}),this.dialogue.bodyNode.one("form").delegate("change",this.options_changed,".searchoptions",this),this.dialogue.visible&&e.later(0,this.dialogue,this.dialogue.centerDialogue),M.question.qbankmanager.init(),this.searchRegionInitialised=!1,this.dialogue.get("visible")&&this.initialiseSearchRegion(),this.dialogue.fire("widget:contentUpdate"),this.dialogue.get("visible")&&(this.dialogue.hide(),this.dialogue.show())},load_failed:function(){},link_clicked:function(e){if(e.currentTarget.ancestor(n.ADDTOQUIZCONTAINER)){e.currentTarget.set("href",e.currentTarget.get("href")+"&addonpage="+this.addonpage);return}if(e.currentTarget.ancestor(n.PREVIEWCONTAINER)){window.openpopup(e,{url:e.currentTarget.get("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"});return}if(e.currentTarget.ancestor(n.SEARCHOPTIONS))return;e.preventDefault(),this.load_content(e.currentTarget.get("search"))},options_changed:function(t){t.preventDefault(),this.load_content("?"+e.IO.stringify(t.currentTarget.get("form")))},initialiseSearchRegion:function(){if(this.searchRegionInitialised===!0)return;if(!e.one(n.SEARCHOPTIONS))return;M.util.init_collapsible_region(e,"advancedsearch","question_bank_advanced_search",M.util.get_string("clicktohideshow","moodle")),this.searchRegionInitialised=!0}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.quizquestionbank=M.mod_quiz.quizquestionbank||{},M.mod_quiz.quizquestionbank.init=function(){return new i}},"@VERSION@",{requires:["base","event","node","io","io-form","yui-later","moodle-question-qbankmanager","moodle-core-notification-dialogue"]});
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
YUI.add('moodle-mod_quiz-quizquestionbank', function (Y, NAME) {
|
||||
|
||||
// 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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Add questions from question bank functionality for a popup in quiz editing page.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
var CSS = {
|
||||
QBANKLOADING: 'div.questionbankloading',
|
||||
ADDQUESTIONLINKS: '.menu [data-action="questionbank"]',
|
||||
ADDTOQUIZCONTAINER: 'td.addtoquizaction',
|
||||
PREVIEWCONTAINER: 'td.previewaction',
|
||||
SEARCHOPTIONS: '#advancedsearch'
|
||||
};
|
||||
|
||||
var PARAMS = {
|
||||
PAGE: 'addonpage',
|
||||
HEADER: 'header'
|
||||
};
|
||||
|
||||
var POPUP = function() {
|
||||
POPUP.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(POPUP, Y.Base, {
|
||||
loadingDiv: '',
|
||||
dialogue: null,
|
||||
addonpage: 0,
|
||||
searchRegionInitialised: false,
|
||||
|
||||
create_dialogue: function() {
|
||||
// Create a dialogue on the page and hide it.
|
||||
var config = {
|
||||
headerContent: '',
|
||||
bodyContent: Y.one(CSS.QBANKLOADING),
|
||||
draggable: true,
|
||||
modal: true,
|
||||
centered: true,
|
||||
width: null,
|
||||
visible: false,
|
||||
postmethod: 'form',
|
||||
footerContent: null,
|
||||
extraClasses: ['mod_quiz_qbank_dialogue']
|
||||
};
|
||||
this.dialogue = new M.core.dialogue(config);
|
||||
this.dialogue.bodyNode.delegate('click', this.link_clicked, 'a[href]', this);
|
||||
this.dialogue.hide();
|
||||
|
||||
this.loadingDiv = this.dialogue.bodyNode.getHTML();
|
||||
|
||||
Y.later(100, this, function() {
|
||||
this.load_content(window.location.search);
|
||||
});
|
||||
},
|
||||
|
||||
initializer: function() {
|
||||
if (!Y.one(CSS.QBANKLOADING)) {
|
||||
return;
|
||||
}
|
||||
this.create_dialogue();
|
||||
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDQUESTIONLINKS, this);
|
||||
},
|
||||
|
||||
display_dialogue: function(e) {
|
||||
e.preventDefault();
|
||||
this.dialogue.set('headerContent', e.currentTarget.getData(PARAMS.HEADER));
|
||||
|
||||
this.addonpage = e.currentTarget.getData(PARAMS.PAGE);
|
||||
var controlsDiv = this.dialogue.bodyNode.one('.modulespecificbuttonscontainer');
|
||||
if (controlsDiv) {
|
||||
var hidden = controlsDiv.one('input[name=addonpage]');
|
||||
if (!hidden) {
|
||||
hidden = controlsDiv.appendChild('<input type="hidden" name="addonpage">');
|
||||
}
|
||||
hidden.set('value', this.addonpage);
|
||||
}
|
||||
|
||||
this.initialiseSearchRegion();
|
||||
this.dialogue.show();
|
||||
},
|
||||
|
||||
load_content: function(queryString) {
|
||||
this.dialogue.bodyNode.append(this.loadingDiv);
|
||||
|
||||
// If to support old IE.
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState(null, '', M.cfg.wwwroot + '/mod/quiz/edit.php' + queryString);
|
||||
}
|
||||
|
||||
Y.io(M.cfg.wwwroot + '/mod/quiz/questionbank.ajax.php' + queryString, {
|
||||
method: 'GET',
|
||||
on: {
|
||||
success: this.load_done,
|
||||
failure: this.load_failed
|
||||
},
|
||||
context: this
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
load_done: function(transactionid, response) {
|
||||
var result = JSON.parse(response.responseText);
|
||||
if (!result.status || result.status !== 'OK') {
|
||||
// Because IIS is useless, Moodle can't send proper HTTP response
|
||||
// codes, so we have to detect failures manually.
|
||||
this.load_failed(transactionid, response);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.dialogue.bodyNode.setHTML(result.contents);
|
||||
Y.use('moodle-question-chooser', function() {
|
||||
M.question.init_chooser({});
|
||||
});
|
||||
this.dialogue.bodyNode.one('form').delegate('change', this.options_changed, '.searchoptions', this);
|
||||
|
||||
if (this.dialogue.visible) {
|
||||
Y.later(0, this.dialogue, this.dialogue.centerDialogue);
|
||||
}
|
||||
M.question.qbankmanager.init();
|
||||
|
||||
this.searchRegionInitialised = false;
|
||||
if (this.dialogue.get('visible')) {
|
||||
this.initialiseSearchRegion();
|
||||
}
|
||||
|
||||
this.dialogue.fire('widget:contentUpdate');
|
||||
// TODO MDL-47602 really, the base class should listen for the even fired
|
||||
// on the previous line, and fix things like makeResponsive.
|
||||
// However, it does not. So the next two lines are a hack to fix up
|
||||
// display issues (e.g. overall scrollbars on the page). Once the base class
|
||||
// is fixed, this comment and the following four lines should be deleted.
|
||||
if (this.dialogue.get('visible')) {
|
||||
this.dialogue.hide();
|
||||
this.dialogue.show();
|
||||
}
|
||||
},
|
||||
|
||||
load_failed: function() {
|
||||
},
|
||||
|
||||
link_clicked: function(e) {
|
||||
// Add question to quiz. mofify the URL, then let it work as normal.
|
||||
if (e.currentTarget.ancestor(CSS.ADDTOQUIZCONTAINER)) {
|
||||
e.currentTarget.set('href', e.currentTarget.get('href') + '&addonpage=' + this.addonpage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Question preview. Needs to open in a pop-up.
|
||||
if (e.currentTarget.ancestor(CSS.PREVIEWCONTAINER)) {
|
||||
window.openpopup(e, {
|
||||
url: e.currentTarget.get('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'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on expand/collaspse search-options. Has its own handler.
|
||||
// We should not interfere.
|
||||
if (e.currentTarget.ancestor(CSS.SEARCHOPTIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else means reload the pop-up contents.
|
||||
e.preventDefault();
|
||||
this.load_content(e.currentTarget.get('search'));
|
||||
},
|
||||
|
||||
options_changed: function(e) {
|
||||
e.preventDefault();
|
||||
this.load_content('?' + Y.IO.stringify(e.currentTarget.get('form')));
|
||||
},
|
||||
|
||||
initialiseSearchRegion: function() {
|
||||
if (this.searchRegionInitialised === true) {
|
||||
return;
|
||||
}
|
||||
if (!Y.one(CSS.SEARCHOPTIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
M.util.init_collapsible_region(Y, "advancedsearch", "question_bank_advanced_search",
|
||||
M.util.get_string('clicktohideshow', 'moodle'));
|
||||
this.searchRegionInitialised = true;
|
||||
}
|
||||
});
|
||||
|
||||
M.mod_quiz = M.mod_quiz || {};
|
||||
M.mod_quiz.quizquestionbank = M.mod_quiz.quizquestionbank || {};
|
||||
M.mod_quiz.quizquestionbank.init = function() {
|
||||
return new POPUP();
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {
|
||||
"requires": [
|
||||
"base",
|
||||
"event",
|
||||
"node",
|
||||
"io",
|
||||
"io-form",
|
||||
"yui-later",
|
||||
"moodle-question-qbankmanager",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "moodle-mod_quiz-quizquestionbank",
|
||||
"builds": {
|
||||
"moodle-mod_quiz-quizquestionbank": {
|
||||
"jsfiles": [
|
||||
"quizquestionbank.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
|
||||
/**
|
||||
* Add questions from question bank functionality for a popup in quiz editing page.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @copyright 2014 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
var CSS = {
|
||||
QBANKLOADING: 'div.questionbankloading',
|
||||
ADDQUESTIONLINKS: '.menu [data-action="questionbank"]',
|
||||
ADDTOQUIZCONTAINER: 'td.addtoquizaction',
|
||||
PREVIEWCONTAINER: 'td.previewaction',
|
||||
SEARCHOPTIONS: '#advancedsearch'
|
||||
};
|
||||
|
||||
var PARAMS = {
|
||||
PAGE: 'addonpage',
|
||||
HEADER: 'header'
|
||||
};
|
||||
|
||||
var POPUP = function() {
|
||||
POPUP.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
|
||||
Y.extend(POPUP, Y.Base, {
|
||||
loadingDiv: '',
|
||||
dialogue: null,
|
||||
addonpage: 0,
|
||||
searchRegionInitialised: false,
|
||||
|
||||
create_dialogue: function() {
|
||||
// Create a dialogue on the page and hide it.
|
||||
var config = {
|
||||
headerContent: '',
|
||||
bodyContent: Y.one(CSS.QBANKLOADING),
|
||||
draggable: true,
|
||||
modal: true,
|
||||
centered: true,
|
||||
width: null,
|
||||
visible: false,
|
||||
postmethod: 'form',
|
||||
footerContent: null,
|
||||
extraClasses: ['mod_quiz_qbank_dialogue']
|
||||
};
|
||||
this.dialogue = new M.core.dialogue(config);
|
||||
this.dialogue.bodyNode.delegate('click', this.link_clicked, 'a[href]', this);
|
||||
this.dialogue.hide();
|
||||
|
||||
this.loadingDiv = this.dialogue.bodyNode.getHTML();
|
||||
|
||||
Y.later(100, this, function() {
|
||||
this.load_content(window.location.search);
|
||||
});
|
||||
},
|
||||
|
||||
initializer: function() {
|
||||
if (!Y.one(CSS.QBANKLOADING)) {
|
||||
return;
|
||||
}
|
||||
this.create_dialogue();
|
||||
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDQUESTIONLINKS, this);
|
||||
},
|
||||
|
||||
display_dialogue: function(e) {
|
||||
e.preventDefault();
|
||||
this.dialogue.set('headerContent', e.currentTarget.getData(PARAMS.HEADER));
|
||||
|
||||
this.addonpage = e.currentTarget.getData(PARAMS.PAGE);
|
||||
var controlsDiv = this.dialogue.bodyNode.one('.modulespecificbuttonscontainer');
|
||||
if (controlsDiv) {
|
||||
var hidden = controlsDiv.one('input[name=addonpage]');
|
||||
if (!hidden) {
|
||||
hidden = controlsDiv.appendChild('<input type="hidden" name="addonpage">');
|
||||
}
|
||||
hidden.set('value', this.addonpage);
|
||||
}
|
||||
|
||||
this.initialiseSearchRegion();
|
||||
this.dialogue.show();
|
||||
},
|
||||
|
||||
load_content: function(queryString) {
|
||||
Y.log('Starting load.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
this.dialogue.bodyNode.append(this.loadingDiv);
|
||||
|
||||
// If to support old IE.
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState(null, '', M.cfg.wwwroot + '/mod/quiz/edit.php' + queryString);
|
||||
}
|
||||
|
||||
Y.io(M.cfg.wwwroot + '/mod/quiz/questionbank.ajax.php' + queryString, {
|
||||
method: 'GET',
|
||||
on: {
|
||||
success: this.load_done,
|
||||
failure: this.load_failed
|
||||
},
|
||||
context: this
|
||||
});
|
||||
|
||||
Y.log('Load request sent.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
},
|
||||
|
||||
load_done: function(transactionid, response) {
|
||||
var result = JSON.parse(response.responseText);
|
||||
if (!result.status || result.status !== 'OK') {
|
||||
// Because IIS is useless, Moodle can't send proper HTTP response
|
||||
// codes, so we have to detect failures manually.
|
||||
this.load_failed(transactionid, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Y.log('Load completed.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
|
||||
this.dialogue.bodyNode.setHTML(result.contents);
|
||||
Y.use('moodle-question-chooser', function() {
|
||||
M.question.init_chooser({});
|
||||
});
|
||||
this.dialogue.bodyNode.one('form').delegate('change', this.options_changed, '.searchoptions', this);
|
||||
|
||||
if (this.dialogue.visible) {
|
||||
Y.later(0, this.dialogue, this.dialogue.centerDialogue);
|
||||
}
|
||||
M.question.qbankmanager.init();
|
||||
|
||||
this.searchRegionInitialised = false;
|
||||
if (this.dialogue.get('visible')) {
|
||||
this.initialiseSearchRegion();
|
||||
}
|
||||
|
||||
this.dialogue.fire('widget:contentUpdate');
|
||||
// TODO MDL-47602 really, the base class should listen for the even fired
|
||||
// on the previous line, and fix things like makeResponsive.
|
||||
// However, it does not. So the next two lines are a hack to fix up
|
||||
// display issues (e.g. overall scrollbars on the page). Once the base class
|
||||
// is fixed, this comment and the following four lines should be deleted.
|
||||
if (this.dialogue.get('visible')) {
|
||||
this.dialogue.hide();
|
||||
this.dialogue.show();
|
||||
}
|
||||
},
|
||||
|
||||
load_failed: function() {
|
||||
Y.log('Load failed.', 'debug', 'moodle-mod_quiz-quizquestionbank');
|
||||
},
|
||||
|
||||
link_clicked: function(e) {
|
||||
// Add question to quiz. mofify the URL, then let it work as normal.
|
||||
if (e.currentTarget.ancestor(CSS.ADDTOQUIZCONTAINER)) {
|
||||
e.currentTarget.set('href', e.currentTarget.get('href') + '&addonpage=' + this.addonpage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Question preview. Needs to open in a pop-up.
|
||||
if (e.currentTarget.ancestor(CSS.PREVIEWCONTAINER)) {
|
||||
window.openpopup(e, {
|
||||
url: e.currentTarget.get('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'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on expand/collaspse search-options. Has its own handler.
|
||||
// We should not interfere.
|
||||
if (e.currentTarget.ancestor(CSS.SEARCHOPTIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else means reload the pop-up contents.
|
||||
e.preventDefault();
|
||||
this.load_content(e.currentTarget.get('search'));
|
||||
},
|
||||
|
||||
options_changed: function(e) {
|
||||
e.preventDefault();
|
||||
this.load_content('?' + Y.IO.stringify(e.currentTarget.get('form')));
|
||||
},
|
||||
|
||||
initialiseSearchRegion: function() {
|
||||
if (this.searchRegionInitialised === true) {
|
||||
return;
|
||||
}
|
||||
if (!Y.one(CSS.SEARCHOPTIONS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
M.util.init_collapsible_region(Y, "advancedsearch", "question_bank_advanced_search",
|
||||
M.util.get_string('clicktohideshow', 'moodle'));
|
||||
this.searchRegionInitialised = true;
|
||||
}
|
||||
});
|
||||
|
||||
M.mod_quiz = M.mod_quiz || {};
|
||||
M.mod_quiz.quizquestionbank = M.mod_quiz.quizquestionbank || {};
|
||||
M.mod_quiz.quizquestionbank.init = function() {
|
||||
return new POPUP();
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"moodle-mod_quiz-quizquestionbank": {
|
||||
"requires": [
|
||||
"base",
|
||||
"event",
|
||||
"node",
|
||||
"io",
|
||||
"io-form",
|
||||
"yui-later",
|
||||
"moodle-question-qbankmanager",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* A condition for adding filtering by tag to the question bank.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_question\bank\search;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Question bank search class to allow searching/filtering by tags on a question.
|
||||
*
|
||||
* @copyright 2018 Ryan Wyllie <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tag_condition extends condition {
|
||||
/** @var string SQL fragment to add to the where clause. */
|
||||
protected $where;
|
||||
/** @var string SQL fragment to add to the where clause. */
|
||||
protected $contexts;
|
||||
/** @var array List of IDs for tags that have been selected in the form. */
|
||||
protected $selectedtagids;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param context[] $contexts List of contexts to show tags from
|
||||
* @param int[] $selectedtagids List of IDs for tags to filter by.
|
||||
*/
|
||||
public function __construct(array $contexts, array $selectedtagids = []) {
|
||||
global $DB;
|
||||
|
||||
$this->contexts = $contexts;
|
||||
|
||||
// If some tags have been selected then we need to filter
|
||||
// the question list by the selected tags.
|
||||
if ($selectedtagids) {
|
||||
// We treat each additional tag as an AND condition rather than
|
||||
// an OR condition.
|
||||
//
|
||||
// For example, if the user filters by the tags "foo" and "bar" then
|
||||
// we reduce the question list to questions that are tagged with both
|
||||
// "foo" AND "bar". Any question that does not have ALL of the specified
|
||||
// tags will be omitted.
|
||||
list($tagsql, $tagparams) = $DB->get_in_or_equal($selectedtagids, SQL_PARAMS_NAMED);
|
||||
$tagparams['tagcount'] = count($selectedtagids);
|
||||
$this->selectedtagids = $selectedtagids;
|
||||
$this->params = $tagparams;
|
||||
$this->where = "q.id IN (SELECT ti.itemid
|
||||
FROM {tag_instance} ti
|
||||
WHERE ti.tagid {$tagsql}
|
||||
GROUP BY ti.itemid
|
||||
HAVING COUNT(itemid) = :tagcount)";
|
||||
|
||||
} else {
|
||||
$this->selectedtagids = [];
|
||||
$this->params = [];
|
||||
$this->where = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL WHERE snippet to be used in the SQL to retrieve the
|
||||
* list of questions. This SQL snippet will add the logic for the
|
||||
* tag condition.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function where() {
|
||||
return $this->where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Named SQL params to be used with the SQL WHERE snippet.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function params() {
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print HTML to display the list of tags to filter by.
|
||||
*/
|
||||
public function display_options() {
|
||||
global $OUTPUT;
|
||||
|
||||
$tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $this->contexts);
|
||||
$tagoptions = array_map(function($tag) {
|
||||
return [
|
||||
'id' => $tag->id,
|
||||
'name' => $tag->name,
|
||||
'selected' => in_array($tag->id, $this->selectedtagids)
|
||||
];
|
||||
}, array_values($tags));
|
||||
$context = [
|
||||
'tagoptions' => $tagoptions
|
||||
];
|
||||
|
||||
echo $OUTPUT->render_from_template('core_question/tag_condition', $context);
|
||||
}
|
||||
}
|
||||
@@ -461,22 +461,25 @@ class view {
|
||||
* displayoptions Sets display options
|
||||
*/
|
||||
public function display($tabname, $page, $perpage, $cat,
|
||||
$recurse, $showhidden, $showquestiontext) {
|
||||
global $PAGE, $OUTPUT;
|
||||
$recurse, $showhidden, $showquestiontext, $tagids = []) {
|
||||
global $PAGE;
|
||||
|
||||
if ($this->process_actions_needing_ui()) {
|
||||
return;
|
||||
}
|
||||
$editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
|
||||
list($categoryid, $contextid) = explode(',', $cat);
|
||||
$catcontext = \context::instance_by_id($contextid);
|
||||
// Category selection form.
|
||||
echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
|
||||
$this->display_question_bank_header();
|
||||
array_unshift($this->searchconditions, new \core_question\bank\search\tag_condition([$catcontext], $tagids));
|
||||
array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
|
||||
array_unshift($this->searchconditions, new \core_question\bank\search\category_condition(
|
||||
$cat, $recurse, $editcontexts, $this->baseurl, $this->course));
|
||||
$this->display_options_form($showquestiontext);
|
||||
|
||||
// Continues with list of questions.
|
||||
$this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname),
|
||||
$this->display_question_list($editcontexts,
|
||||
$this->baseurl, $cat, $this->cm,
|
||||
null, $page, $perpage, $showhidden, $showquestiontext,
|
||||
$this->contexts->having_cap('moodle/question:add'));
|
||||
@@ -594,7 +597,19 @@ class view {
|
||||
echo \html_writer::start_tag('form', array('method' => 'get',
|
||||
'action' => new \moodle_url($scriptpath), 'id' => 'displayoptions'));
|
||||
echo \html_writer::start_div();
|
||||
echo \html_writer::input_hidden_params($this->baseurl, array('recurse', 'showhidden', 'qbshowtext'));
|
||||
|
||||
$excludes = array('recurse', 'showhidden', 'qbshowtext');
|
||||
// If the URL contains any tags then we need to prevent them
|
||||
// being added to the form as hidden elements because the tags
|
||||
// are managed separately.
|
||||
if ($this->baseurl->param('qtagids[0]')) {
|
||||
$index = 0;
|
||||
while ($this->baseurl->param("qtagids[{$index}]")) {
|
||||
$excludes[] = "qtagids[{$index}]";
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
echo \html_writer::input_hidden_params($this->baseurl, $excludes);
|
||||
|
||||
foreach ($this->searchconditions as $searchcondition) {
|
||||
echo $searchcondition->display_options($this);
|
||||
@@ -635,6 +650,14 @@ class view {
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the header element for the question bank.
|
||||
*/
|
||||
protected function display_question_bank_header() {
|
||||
global $OUTPUT;
|
||||
echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
|
||||
}
|
||||
|
||||
protected function create_new_question_form($category, $canadd) {
|
||||
global $CFG;
|
||||
echo '<div class="createnewquestion">';
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ echo $renderer->extra_horizontal_navigation();
|
||||
echo '<div class="questionbankwindow boxwidthwide boxaligncenter">';
|
||||
$questionbank->display('questions', $pagevars['qpage'], $pagevars['qperpage'],
|
||||
$pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'],
|
||||
$pagevars['qbshowtext']);
|
||||
$pagevars['qbshowtext'], $pagevars['qtagids']);
|
||||
echo "</div>\n";
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
+199
-40
@@ -278,21 +278,150 @@ class_alias('core_question\bank\view', 'question_bank_view', true);
|
||||
* @return array $thispageurl, $contexts, $cmid, $cm, $module, $pagevars
|
||||
*/
|
||||
function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused = null) {
|
||||
global $DB, $PAGE, $CFG;
|
||||
global $PAGE;
|
||||
|
||||
if ($unused !== null) {
|
||||
debugging('Deprecated argument passed to question_edit_setup()', DEBUG_DEVELOPER);
|
||||
}
|
||||
|
||||
$params = [];
|
||||
|
||||
if ($requirecmid) {
|
||||
$params['cmid'] = required_param('cmid', PARAM_INT);
|
||||
} else {
|
||||
$params['cmid'] = optional_param('cmid', null, PARAM_INT);
|
||||
}
|
||||
|
||||
if (!$params['cmid']) {
|
||||
$params['courseid'] = required_param('courseid', PARAM_INT);
|
||||
}
|
||||
|
||||
$params['qpage'] = optional_param('qpage', null, PARAM_INT);
|
||||
|
||||
// Pass 'cat' from page to page and when 'category' comes from a drop down menu
|
||||
// then we also reset the qpage so we go to page 1 of
|
||||
// a new cat.
|
||||
$params['cat'] = optional_param('cat', null, PARAM_SEQUENCE); // If empty will be set up later.
|
||||
$params['category'] = optional_param('category', null, PARAM_SEQUENCE);
|
||||
$params['qperpage'] = optional_param('qperpage', null, PARAM_INT);
|
||||
|
||||
// Question table sorting options.
|
||||
for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
|
||||
$param = 'qbs' . $i;
|
||||
if ($sort = optional_param($param, '', PARAM_TEXT)) {
|
||||
$params[$param] = $sort;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Display options.
|
||||
$params['recurse'] = optional_param('recurse', null, PARAM_BOOL);
|
||||
$params['showhidden'] = optional_param('showhidden', null, PARAM_BOOL);
|
||||
$params['qbshowtext'] = optional_param('qbshowtext', null, PARAM_BOOL);
|
||||
// Category list page.
|
||||
$params['cpage'] = optional_param('cpage', null, PARAM_INT);
|
||||
$params['qtagids'] = optional_param_array('qtagids', null, PARAM_INT);
|
||||
|
||||
$PAGE->set_pagelayout('admin');
|
||||
|
||||
return question_build_edit_resources($edittab, $baseurl, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common function for building the generic resources required by the
|
||||
* editing questions pages.
|
||||
*
|
||||
* Either a cmid or a course id must be provided as keys in $params or
|
||||
* an exception will be thrown. All other params are optional and will have
|
||||
* sane default applied if not provided.
|
||||
*
|
||||
* The acceptable keys for $params are:
|
||||
* [
|
||||
* 'cmid' => PARAM_INT,
|
||||
* 'courseid' => PARAM_INT,
|
||||
* 'qpage' => PARAM_INT,
|
||||
* 'cat' => PARAM_SEQUENCE,
|
||||
* 'category' => PARAM_SEQUENCE,
|
||||
* 'qperpage' => PARAM_INT,
|
||||
* 'recurse' => PARAM_INT,
|
||||
* 'showhidden' => PARAM_INT,
|
||||
* 'qbshowtext' => PARAM_INT,
|
||||
* 'cpage' => PARAM_INT,
|
||||
* 'recurse' => PARAM_BOOL,
|
||||
* 'showhidden' => PARAM_BOOL,
|
||||
* 'qbshowtext' => PARAM_BOOL,
|
||||
* 'qtagids' => [PARAM_INT], (array of integers)
|
||||
* 'qbs1' => PARAM_TEXT,
|
||||
* 'qbs2' => PARAM_TEXT,
|
||||
* 'qbs3' => PARAM_TEXT,
|
||||
* ... and more qbs keys up to question_bank_view::MAX_SORTS ...
|
||||
* ];
|
||||
*
|
||||
* @param string $edittab Code for this edit tab
|
||||
* @param string $baseurl The name of the script calling this funciton. For examle 'qusetion/edit.php'.
|
||||
* @param array $params The provided parameters to construct the resources with.
|
||||
* @return array $thispageurl, $contexts, $cmid, $cm, $module, $pagevars
|
||||
*/
|
||||
function question_build_edit_resources($edittab, $baseurl, $params) {
|
||||
global $DB, $PAGE, $CFG;
|
||||
|
||||
$thispageurl = new moodle_url($baseurl);
|
||||
$thispageurl->remove_all_params(); // We are going to explicity add back everything important - this avoids unwanted params from being retained.
|
||||
|
||||
if ($requirecmid){
|
||||
$cmid =required_param('cmid', PARAM_INT);
|
||||
} else {
|
||||
$cmid = optional_param('cmid', 0, PARAM_INT);
|
||||
$cleanparams = [
|
||||
'qsorts' => [],
|
||||
'qtagids' => []
|
||||
];
|
||||
$paramtypes = [
|
||||
'cmid' => PARAM_INT,
|
||||
'courseid' => PARAM_INT,
|
||||
'qpage' => PARAM_INT,
|
||||
'cat' => PARAM_SEQUENCE,
|
||||
'category' => PARAM_SEQUENCE,
|
||||
'qperpage' => PARAM_INT,
|
||||
'recurse' => PARAM_INT,
|
||||
'showhidden' => PARAM_INT,
|
||||
'qbshowtext' => PARAM_INT,
|
||||
'cpage' => PARAM_INT,
|
||||
'recurse' => PARAM_BOOL,
|
||||
'showhidden' => PARAM_BOOL,
|
||||
'qbshowtext' => PARAM_BOOL
|
||||
];
|
||||
|
||||
foreach ($paramtypes as $name => $type) {
|
||||
if (isset($params[$name])) {
|
||||
$cleanparams[$name] = clean_param($params[$name], $type);
|
||||
} else {
|
||||
$cleanparams[$name] = null;
|
||||
}
|
||||
}
|
||||
if ($cmid){
|
||||
|
||||
if (!empty($params['qtagids'])) {
|
||||
$cleanparams['qtagids'] = clean_param_array($params['qtagids'], PARAM_INT);
|
||||
}
|
||||
|
||||
$cmid = $cleanparams['cmid'];
|
||||
$courseid = $cleanparams['courseid'];
|
||||
$qpage = $cleanparams['qpage'] ?: -1;
|
||||
$cat = $cleanparams['cat'] ?: 0;
|
||||
$category = $cleanparams['category'] ?: 0;
|
||||
$qperpage = $cleanparams['qperpage'];
|
||||
$recurse = $cleanparams['recurse'];
|
||||
$showhidden = $cleanparams['showhidden'];
|
||||
$qbshowtext = $cleanparams['qbshowtext'];
|
||||
$cpage = $cleanparams['cpage'] ?: 1;
|
||||
$recurse = $cleanparams['recurse'];
|
||||
$showhidden = $cleanparams['showhidden'];
|
||||
$qbshowtext = $cleanparams['qbshowtext'];
|
||||
$qsorts = $cleanparams['qsorts'];
|
||||
$qtagids = $cleanparams['qtagids'];
|
||||
|
||||
if (is_null($cmid) && is_null($courseid)) {
|
||||
throw new \moodle_exception('Must provide a cmid or courseid');
|
||||
}
|
||||
|
||||
if ($cmid) {
|
||||
list($module, $cm) = get_module_from_cmid($cmid);
|
||||
$courseid = $cm->course;
|
||||
$thispageurl->params(compact('cmid'));
|
||||
@@ -301,7 +430,6 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused =
|
||||
} else {
|
||||
$module = null;
|
||||
$cm = null;
|
||||
$courseid = required_param('courseid', PARAM_INT);
|
||||
$thispageurl->params(compact('courseid'));
|
||||
require_login($courseid, false);
|
||||
$thiscontext = context_course::instance($courseid);
|
||||
@@ -310,52 +438,55 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused =
|
||||
if ($thiscontext){
|
||||
$contexts = new question_edit_contexts($thiscontext);
|
||||
$contexts->require_one_edit_tab_cap($edittab);
|
||||
|
||||
} else {
|
||||
$contexts = null;
|
||||
}
|
||||
|
||||
$PAGE->set_pagelayout('admin');
|
||||
$pagevars['qpage'] = $qpage;
|
||||
|
||||
$pagevars['qpage'] = optional_param('qpage', -1, PARAM_INT);
|
||||
|
||||
//pass 'cat' from page to page and when 'category' comes from a drop down menu
|
||||
//then we also reset the qpage so we go to page 1 of
|
||||
//a new cat.
|
||||
$pagevars['cat'] = optional_param('cat', 0, PARAM_SEQUENCE); // if empty will be set up later
|
||||
if ($category = optional_param('category', 0, PARAM_SEQUENCE)) {
|
||||
if ($pagevars['cat'] != $category) { // is this a move to a new category?
|
||||
$pagevars['cat'] = $category;
|
||||
$pagevars['qpage'] = 0;
|
||||
}
|
||||
// Pass 'cat' from page to page and when 'category' comes from a drop down menu
|
||||
// then we also reset the qpage so we go to page 1 of
|
||||
// a new cat.
|
||||
if ($category && $category != $cat) { // Is this a move to a new category?
|
||||
$pagevars['cat'] = $category;
|
||||
$pagevars['qpage'] = 0;
|
||||
} else {
|
||||
$pagevars['cat'] = $cat; // If empty will be set up later.
|
||||
}
|
||||
|
||||
if ($pagevars['cat']){
|
||||
$thispageurl->param('cat', $pagevars['cat']);
|
||||
}
|
||||
|
||||
if (strpos($baseurl, '/question/') === 0) {
|
||||
navigation_node::override_active_url($thispageurl);
|
||||
}
|
||||
|
||||
// This need to occur after the override_active_url call above because
|
||||
// these values change on the page request causing the URLs to mismatch
|
||||
// when trying to work out the active node.
|
||||
for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
|
||||
$param = 'qbs' . $i;
|
||||
if (isset($params[$param])) {
|
||||
$value = clean_param($params[$param], PARAM_TEXT);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
$thispageurl->param($param, $value);
|
||||
}
|
||||
|
||||
if ($pagevars['qpage'] > -1) {
|
||||
$thispageurl->param('qpage', $pagevars['qpage']);
|
||||
} else {
|
||||
$pagevars['qpage'] = 0;
|
||||
}
|
||||
|
||||
$pagevars['qperpage'] = question_get_display_preference(
|
||||
'qperpage', DEFAULT_QUESTIONS_PER_PAGE, PARAM_INT, $thispageurl);
|
||||
|
||||
for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
|
||||
$param = 'qbs' . $i;
|
||||
if (!$sort = optional_param($param, '', PARAM_TEXT)) {
|
||||
break;
|
||||
}
|
||||
$thispageurl->param($param, $sort);
|
||||
}
|
||||
$pagevars['qperpage'] = question_build_display_preference(
|
||||
'qperpage', $qperpage, DEFAULT_QUESTIONS_PER_PAGE, $thispageurl);
|
||||
|
||||
$defaultcategory = question_make_default_categories($contexts->all());
|
||||
|
||||
$contextlistarr = array();
|
||||
$contextlistarr = [];
|
||||
foreach ($contexts->having_one_edit_tab_cap($edittab) as $context){
|
||||
$contextlistarr[] = "'{$context->id}'";
|
||||
}
|
||||
@@ -372,16 +503,21 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused =
|
||||
}
|
||||
|
||||
// Display options.
|
||||
$pagevars['recurse'] = question_get_display_preference('recurse', 1, PARAM_BOOL, $thispageurl);
|
||||
$pagevars['showhidden'] = question_get_display_preference('showhidden', 0, PARAM_BOOL, $thispageurl);
|
||||
$pagevars['qbshowtext'] = question_get_display_preference('qbshowtext', 0, PARAM_BOOL, $thispageurl);
|
||||
$pagevars['recurse'] = question_build_display_preference('recurse', $recurse, 1, $thispageurl);
|
||||
$pagevars['showhidden'] = question_build_display_preference('showhidden', $showhidden, 0, $thispageurl);
|
||||
$pagevars['qbshowtext'] = question_build_display_preference('qbshowtext', $qbshowtext, 0, $thispageurl);
|
||||
|
||||
// Category list page.
|
||||
$pagevars['cpage'] = optional_param('cpage', 1, PARAM_INT);
|
||||
$pagevars['cpage'] = $cpage;
|
||||
if ($pagevars['cpage'] != 1){
|
||||
$thispageurl->param('cpage', $pagevars['cpage']);
|
||||
}
|
||||
|
||||
$pagevars['qtagids'] = $qtagids;
|
||||
foreach ($pagevars['qtagids'] as $index => $qtagid) {
|
||||
$thispageurl->param("qtagids[{$index}]", $qtagid);
|
||||
}
|
||||
|
||||
return array($thispageurl, $contexts, $cmid, $cm, $module, $pagevars);
|
||||
}
|
||||
|
||||
@@ -412,13 +548,36 @@ function question_get_category_id_from_pagevars(array $pagevars) {
|
||||
*/
|
||||
function question_get_display_preference($param, $default, $type, $thispageurl) {
|
||||
$submittedvalue = optional_param($param, null, $type);
|
||||
if (is_null($submittedvalue)) {
|
||||
return get_user_preferences('question_bank_' . $param, $default);
|
||||
return question_build_display_preference($param, $submittedvalue, $default, $thispageurl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user preference by name or set the user preference to a given value.
|
||||
*
|
||||
* If $value is null then the function will only attempt to retrieve the
|
||||
* user preference requested by $name. If no user preference is found then the
|
||||
* $default value will be returned. In this case the user preferences are not
|
||||
* modified and nor are the params on $thispageurl.
|
||||
*
|
||||
* If $value is anything other than null then the function will set the user
|
||||
* preference $name to the provided $value and will also set it as a param
|
||||
* on $thispageurl.
|
||||
*
|
||||
* @param string $name The user_preference name is 'question_bank_' . $name.
|
||||
* @param mixed $value The preference value.
|
||||
* @param mixed $default The default value to use, if not otherwise set.
|
||||
* @param moodle_url $thispageurl if the value has been explicitly set, we add
|
||||
* it to this URL.
|
||||
* @return mixed the parameter value to use.
|
||||
*/
|
||||
function question_build_display_preference($name, $value, $default, $thispageurl) {
|
||||
if (is_null($value)) {
|
||||
return get_user_preferences('question_bank_' . $name, $default);
|
||||
}
|
||||
|
||||
set_user_preference('question_bank_' . $param, $submittedvalue);
|
||||
$thispageurl->param($param, $submittedvalue);
|
||||
return $submittedvalue;
|
||||
set_user_preference('question_bank_' . $name, $value);
|
||||
$thispageurl->param($name, $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
{{!
|
||||
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 core_question/tag_condition
|
||||
|
||||
An auto-complete select box containing a list of available tags to
|
||||
filter the quesiton bank questions by.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* tagoptions A list of available tags
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"tagoptions": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "foo",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "bar",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="tag-condition-container" data-region="tag-condition-container-{{uniqid}}">
|
||||
<div class="form-group">
|
||||
<select multiple name="qtagids[]" class="form-control invisible" size="3" data-region="tag-select">
|
||||
{{#tagoptions}}
|
||||
<option {{#selected}}selected{{/selected}} value="{{id}}">{{name}}</option>
|
||||
{{/tagoptions}}
|
||||
</select>
|
||||
{{< core/overlay_loading }}
|
||||
{{$hiddenclass}}{{/hiddenclass}}
|
||||
{{/ core/overlay_loading }}
|
||||
</div>
|
||||
</div>
|
||||
{{#js}}
|
||||
require(
|
||||
[
|
||||
'jquery',
|
||||
'core/form-autocomplete'
|
||||
],
|
||||
function(
|
||||
$,
|
||||
AutoComplete
|
||||
) {
|
||||
var root = $('[data-region="tag-condition-container-{{uniqid}}"]');
|
||||
var selectElement = root.find('[data-region="tag-select"]');
|
||||
var loadingContainer = root.find('[data-region="overlay-icon-container"]');
|
||||
var placeholderText = '{{#str}} filterbytags, core_question {{/str}}';
|
||||
var noSelectionText = '{{#str}} notagfiltersapplied, core_question {{/str}}';
|
||||
|
||||
AutoComplete.enhance(
|
||||
selectElement, // Element to enhance.
|
||||
false, // Don't allow support for creating new tags.
|
||||
false, // Don't allow AMD module to handle loading new tags.
|
||||
placeholderText, // Placeholder text.
|
||||
false, // Make search case insensitive.
|
||||
true, // Show suggestions for tags.
|
||||
noSelectionText // Text when no tags are selected.
|
||||
).always(function() {
|
||||
// Hide the loading icon once the autocomplete has initialised.
|
||||
loadingContainer.addClass('hidden');
|
||||
});
|
||||
|
||||
// We need to trigger a form submission because of how the question bank
|
||||
// page handles reloading the questions when an option changes.
|
||||
selectElement.on('change', function() {
|
||||
selectElement.closest('form').submit();
|
||||
});
|
||||
});
|
||||
{{/js}}
|
||||
@@ -0,0 +1,41 @@
|
||||
@core @core_question
|
||||
Feature: The questions in the question bank can be filtered by tags
|
||||
In order to find the questions I need
|
||||
As a teacher
|
||||
I want to filter the questions by tags
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | user | questiontext |
|
||||
| Test questions | essay | question 1 name | admin | Question 1 text |
|
||||
| Test questions | essay | question 2 name | teacher1 | Question 2 text |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Questions" node in "Course administration > Question bank"
|
||||
And I click on "Edit" "link" in the "question 1 name" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Tags | foo |
|
||||
And I press "id_submitbutton"
|
||||
And I click on "Edit" "link" in the "question 2 name" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Tags | bar |
|
||||
And I press "id_submitbutton"
|
||||
|
||||
@javascript
|
||||
Scenario: The questions can be filtered by tag
|
||||
When I set the field "Filter by tags..." to "foo"
|
||||
And I press key "13" in the field "Filter by tags..."
|
||||
Then I should see "question 1 name" in the "categoryquestions" "table"
|
||||
And I should not see "question 2 name" in the "categoryquestions" "table"
|
||||
@@ -1604,4 +1604,38 @@ class core_tag_tag {
|
||||
// Finally delete all tags that we combined into the current one.
|
||||
self::delete_tags($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of tags that have been used to tag the given $component
|
||||
* and $itemtype in the provided $contexts.
|
||||
*
|
||||
* @param string $component The tag instance component
|
||||
* @param string $itemtype The tag instance item type
|
||||
* @param context[] $contexts The list of contexts to look for tag instances in
|
||||
* @return core_tag_tag[]
|
||||
*/
|
||||
public static function get_tags_by_area_in_contexts($component, $itemtype, array $contexts) {
|
||||
global $DB;
|
||||
|
||||
$params = [$component, $itemtype];
|
||||
$contextids = array_map(function($context) {
|
||||
return $context->id;
|
||||
}, $contexts);
|
||||
list($contextsql, $contextsqlparams) = $DB->get_in_or_equal($contextids);
|
||||
$params = array_merge($params, $contextsqlparams);
|
||||
|
||||
$subsql = "SELECT tagid
|
||||
FROM {tag_instance}
|
||||
WHERE component = ?
|
||||
AND itemtype = ?
|
||||
AND contextid {$contextsql}
|
||||
GROUP BY tagid";
|
||||
$sql = "SELECT *
|
||||
FROM {tag}
|
||||
WHERE id IN ({$subsql})";
|
||||
|
||||
return array_map(function($record) {
|
||||
return new core_tag_tag($record);
|
||||
}, $DB->get_records_sql($sql, $params));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1053,6 +1053,125 @@ class core_tag_taglib_testcase extends advanced_testcase {
|
||||
$this->assertEquals(['dogs', 'hippo'], $correlatedtags);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_tags_by_area_in_contexts should return an empty array if there
|
||||
* are no tag instances for the area in the given context.
|
||||
*/
|
||||
public function test_get_tags_by_area_in_contexts_empty() {
|
||||
$tagnames = ['foo'];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
$component = 'core';
|
||||
$itemtype = 'user';
|
||||
|
||||
$result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_tags_by_area_in_contexts should return an array of tags that
|
||||
* have instances in the given context even when there is only a single
|
||||
* instance.
|
||||
*/
|
||||
public function test_get_tags_by_area_in_contexts_single_tag_one_context() {
|
||||
$tagnames = ['foo'];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
$component = 'core';
|
||||
$itemtype = 'user';
|
||||
core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, $tagnames);
|
||||
|
||||
$result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
|
||||
$expected = array_map(function($t) {
|
||||
return $t->id;
|
||||
}, $tags);
|
||||
$actual = array_map(function($t) {
|
||||
return $t->id;
|
||||
}, $result);
|
||||
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_tags_by_area_in_contexts should return all tags in an array
|
||||
* that have tag instances in for the area in the given context and
|
||||
* should ignore all tags that don't have an instance.
|
||||
*/
|
||||
public function test_get_tags_by_area_in_contexts_multiple_tags_one_context() {
|
||||
$tagnames = ['foo', 'bar', 'baz'];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
$component = 'core';
|
||||
$itemtype = 'user';
|
||||
core_tag_tag::set_item_tags($component, $itemtype, $user->id, $context, array_slice($tagnames, 0, 2));
|
||||
|
||||
$result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context]);
|
||||
$expected = ['foo', 'bar'];
|
||||
$actual = array_map(function($t) {
|
||||
return $t->name;
|
||||
}, $result);
|
||||
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_tags_by_area_in_contexts should return the unique set of
|
||||
* tags for a area in the given contexts. Multiple tag instances of
|
||||
* the same tag don't result in duplicates in the result set.
|
||||
*
|
||||
* Tags with tag instances in the same area with in difference contexts
|
||||
* should be ignored.
|
||||
*/
|
||||
public function test_get_tags_by_area_in_contexts_multiple_tags_multiple_contexts() {
|
||||
$tagnames = ['foo', 'bar', 'baz', 'bop', 'bam', 'bip'];
|
||||
$collid = core_tag_collection::get_default();
|
||||
$tags = core_tag_tag::create_if_missing($collid, $tagnames);
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$context1 = context_user::instance($user1->id);
|
||||
$context2 = context_user::instance($user2->id);
|
||||
$context3 = context_user::instance($user3->id);
|
||||
$component = 'core';
|
||||
$itemtype = 'user';
|
||||
|
||||
// User 1 tags: 'foo', 'bar'.
|
||||
core_tag_tag::set_item_tags($component, $itemtype, $user1->id, $context1, array_slice($tagnames, 0, 2));
|
||||
// User 2 tags: 'bar', 'baz'.
|
||||
core_tag_tag::set_item_tags($component, $itemtype, $user2->id, $context2, array_slice($tagnames, 1, 2));
|
||||
// User 3 tags: 'bop', 'bam'.
|
||||
core_tag_tag::set_item_tags($component, $itemtype, $user3->id, $context3, array_slice($tagnames, 3, 2));
|
||||
|
||||
$result = core_tag_tag::get_tags_by_area_in_contexts($component, $itemtype, [$context1, $context2]);
|
||||
// Both User 1 and 2 have tagged using 'bar' but we don't
|
||||
// expect duplicate tags in the result since they are the same
|
||||
// tag.
|
||||
//
|
||||
// User 3 has tagged 'bop' and 'bam' but we aren't searching in
|
||||
// that context so they shouldn't be in the results.
|
||||
$expected = ['foo', 'bar', 'baz'];
|
||||
$actual = array_map(function($t) {
|
||||
return $t->name;
|
||||
}, $result);
|
||||
|
||||
sort($expected);
|
||||
sort($actual);
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Help method to return sorted array of names of correlated tags to use for assertions
|
||||
* @param core_tag $tag
|
||||
|
||||
@@ -2179,9 +2179,9 @@ $footer-link-color: $bg-inverse-link-color !default;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,6 +622,10 @@ body.path-question-type .mform fieldset.hidden {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.tag-condition-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.que .info {
|
||||
float: none;
|
||||
|
||||
@@ -2404,9 +2404,9 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions {
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,3 +541,7 @@ body.path-question-type .mform fieldset.hidden {
|
||||
padding: 0;
|
||||
margin: 0.7em 0 0;
|
||||
}
|
||||
|
||||
.tag-condition-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -4795,8 +4795,9 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.overlay-icon-container .loading-icon .icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
font-size: 30px;
|
||||
}
|
||||
[data-drag-type="move"] {
|
||||
cursor: move;
|
||||
@@ -9684,6 +9685,9 @@ body.path-question-type .mform fieldset.hidden {
|
||||
padding: 0;
|
||||
margin: 0.7em 0 0;
|
||||
}
|
||||
.tag-condition-container {
|
||||
position: relative;
|
||||
}
|
||||
/* user.less */
|
||||
.userprofile .fullprofilelink {
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user