Merge branch 'MDL-61135-master' of git://github.com/ryanwyllie/moodle

This commit is contained in:
Andrew Nicols
2018-02-12 11:34:56 +08:00
35 changed files with 1235 additions and 812 deletions
+2
View File
@@ -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.';
+1 -1
View File
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -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, {})
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1
View File
@@ -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
View File
@@ -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;
});
+77
View File
@@ -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);
}
};
});
+5 -5
View File
@@ -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;
}
}
}
+41
View File
@@ -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);
}
-49
View File
@@ -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"
+3
View File
@@ -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
@@ -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"
]
});
@@ -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"]});
@@ -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"
]
}
}
}
-215
View File
@@ -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);
}
}
+28 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
/**
+95
View File
@@ -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"
+34
View File
@@ -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));
}
}
+119
View File
@@ -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
+3 -3
View File
@@ -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;
}
}
}
+4
View File
@@ -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;
+3 -3
View File
@@ -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;
}
+6 -2
View File
@@ -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;