diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 47c8f968346..96b5dbe84d5 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1960,6 +1960,7 @@ class core_plugin_manager { 'previewquestion', 'statistics', 'tagquestion', + 'usage', 'viewcreator', 'viewquestionname', 'viewquestiontext', diff --git a/question/bank/usage/amd/build/usage.min.js b/question/bank/usage/amd/build/usage.min.js new file mode 100644 index 00000000000..d6a62411204 --- /dev/null +++ b/question/bank/usage/amd/build/usage.min.js @@ -0,0 +1,2 @@ +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("qbank_usage/usage",["exports","core/fragment","core/str","core/modal_factory","core/notification"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=h(b);c=g(c);d=h(d);e=h(e);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a){return a&&a.__esModule?a:{default:a}}var i=function(a,c){return b.default.loadFragment("qbank_usage","question_usage",c,a)},j=function(a,b){var f={questionid:a};d.default.create({type:d.default.types.CANCEL,title:c.get_string("usageheader","qbank_usage"),body:i(f,b),large:!0}).then(function(a){a.show();a.getRoot().on("click","a[href].page-link",function(c){c.preventDefault();var d=c.target.getAttribute("href");if("#"!==d){f.querystring=d;a.setBody(i(f,b))}});return a}).fail(e.default.exception)};a.init=function init(a,b){var c=document.querySelector(a),d=c.getAttribute("data-questionid");c.addEventListener("click",function(){j(d,b)})}}); +//# sourceMappingURL=usage.min.js.map diff --git a/question/bank/usage/amd/build/usage.min.js.map b/question/bank/usage/amd/build/usage.min.js.map new file mode 100644 index 00000000000..22d6549b9c4 --- /dev/null +++ b/question/bank/usage/amd/build/usage.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/usage.js"],"names":["getFragment","args","contextId","Fragment","loadFragment","usageEvent","questionId","questionid","ModalFactory","create","type","types","CANCEL","title","Str","get_string","body","large","then","modal","show","getRoot","on","e","preventDefault","attr","target","getAttribute","querystring","setBody","fail","Notification","exception","init","questionSelector","document","querySelector","addEventListener"],"mappings":"+eAwBA,OACA,OACA,OACA,O,4lBAUMA,CAAAA,CAAW,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAqB,CACrC,MAAOC,WAASC,YAAT,CAAsB,aAAtB,CAAqC,gBAArC,CAAuDF,CAAvD,CAAkED,CAAlE,CACV,C,CASKI,CAAU,CAAG,SAACC,CAAD,CAAaJ,CAAb,CAA2B,CAC1C,GAAID,CAAAA,CAAI,CAAG,CACPM,UAAU,CAAED,CADL,CAAX,CAGAE,UAAaC,MAAb,CAAoB,CAChBC,IAAI,CAAEF,UAAaG,KAAb,CAAmBC,MADT,CAEhBC,KAAK,CAAEC,CAAG,CAACC,UAAJ,CAAe,aAAf,CAA8B,aAA9B,CAFS,CAGhBC,IAAI,CAAEhB,CAAW,CAACC,CAAD,CAAOC,CAAP,CAHD,CAIhBe,KAAK,GAJW,CAApB,EAKGC,IALH,CAKQ,SAACC,CAAD,CAAW,CACfA,CAAK,CAACC,IAAN,GACAD,CAAK,CAACE,OAAN,GAAgBC,EAAhB,CAAmB,OAAnB,CAA4B,mBAA5B,CAAiD,SAASC,CAAT,CAAY,CACzDA,CAAC,CAACC,cAAF,GACA,GAAIC,CAAAA,CAAI,CAAGF,CAAC,CAACG,MAAF,CAASC,YAAT,CAAsB,MAAtB,CAAX,CACA,GAAa,GAAT,GAAAF,CAAJ,CAAkB,CACdxB,CAAI,CAAC2B,WAAL,CAAmBH,CAAnB,CACAN,CAAK,CAACU,OAAN,CAAc7B,CAAW,CAACC,CAAD,CAAOC,CAAP,CAAzB,CACH,CACJ,CAPD,EAQA,MAAOiB,CAAAA,CACV,CAhBD,EAgBGW,IAhBH,CAgBQC,UAAaC,SAhBrB,CAiBH,C,QASmB,QAAPC,CAAAA,IAAO,CAACC,CAAD,CAAmBhC,CAAnB,CAAiC,IAC7CwB,CAAAA,CAAM,CAAGS,QAAQ,CAACC,aAAT,CAAuBF,CAAvB,CADoC,CAE7C5B,CAAU,CAAGoB,CAAM,CAACC,YAAP,CAAoB,iBAApB,CAFgC,CAGjDD,CAAM,CAACW,gBAAP,CAAwB,OAAxB,CAAiC,UAAM,CAEnChC,CAAU,CAACC,CAAD,CAAaJ,CAAb,CACb,CAHD,CAIH,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Usage column selector js.\n *\n * @module qbank_usage/usage\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Safat Shahin \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Fragment from 'core/fragment';\nimport * as Str from 'core/str';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\n\n/**\n * Get the fragment.\n *\n * @method getFragment\n * @param {{questioned: int}} args\n * @param {int} contextId\n * @return {string}\n */\nconst getFragment = (args, contextId) => {\n return Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args);\n};\n\n/**\n * Event listeners for the module.\n *\n * @method clickEvent\n * @param {int} questionId\n * @param {int} contextId\n */\nconst usageEvent = (questionId, contextId) => {\n let args = {\n questionid: questionId\n };\n ModalFactory.create({\n type: ModalFactory.types.CANCEL,\n title: Str.get_string('usageheader', 'qbank_usage'),\n body: getFragment(args, contextId),\n large: true,\n }).then((modal) => {\n modal.show();\n modal.getRoot().on('click', 'a[href].page-link', function(e) {\n e.preventDefault();\n let attr = e.target.getAttribute(\"href\");\n if (attr !== '#') {\n args.querystring = attr;\n modal.setBody(getFragment(args, contextId));\n }\n });\n return modal;\n }).fail(Notification.exception);\n};\n\n/**\n * Entrypoint of the js.\n *\n * @method init\n * @param {string} questionSelector the question usage identifier.\n * @param {int} contextId the question context id.\n */\nexport const init = (questionSelector, contextId) => {\n let target = document.querySelector(questionSelector);\n let questionId = target.getAttribute('data-questionid');\n target.addEventListener('click', () => {\n // Call for the event listener to listed for clicks in any usage count row.\n usageEvent(questionId, contextId);\n });\n};\n"],"file":"usage.min.js"} \ No newline at end of file diff --git a/question/bank/usage/amd/src/usage.js b/question/bank/usage/amd/src/usage.js new file mode 100644 index 00000000000..bdb72d6a977 --- /dev/null +++ b/question/bank/usage/amd/src/usage.js @@ -0,0 +1,86 @@ +// 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 . + +/** + * Usage column selector js. + * + * @module qbank_usage/usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Fragment from 'core/fragment'; +import * as Str from 'core/str'; +import ModalFactory from 'core/modal_factory'; +import Notification from 'core/notification'; + +/** + * Get the fragment. + * + * @method getFragment + * @param {{questioned: int}} args + * @param {int} contextId + * @return {string} + */ +const getFragment = (args, contextId) => { + return Fragment.loadFragment('qbank_usage', 'question_usage', contextId, args); +}; + +/** + * Event listeners for the module. + * + * @method clickEvent + * @param {int} questionId + * @param {int} contextId + */ +const usageEvent = (questionId, contextId) => { + let args = { + questionid: questionId + }; + ModalFactory.create({ + type: ModalFactory.types.CANCEL, + title: Str.get_string('usageheader', 'qbank_usage'), + body: getFragment(args, contextId), + large: true, + }).then((modal) => { + modal.show(); + modal.getRoot().on('click', 'a[href].page-link', function(e) { + e.preventDefault(); + let attr = e.target.getAttribute("href"); + if (attr !== '#') { + args.querystring = attr; + modal.setBody(getFragment(args, contextId)); + } + }); + return modal; + }).fail(Notification.exception); +}; + +/** + * Entrypoint of the js. + * + * @method init + * @param {string} questionSelector the question usage identifier. + * @param {int} contextId the question context id. + */ +export const init = (questionSelector, contextId) => { + let target = document.querySelector(questionSelector); + let questionId = target.getAttribute('data-questionid'); + target.addEventListener('click', () => { + // Call for the event listener to listed for clicks in any usage count row. + usageEvent(questionId, contextId); + }); +}; diff --git a/question/bank/usage/classes/helper.php b/question/bank/usage/classes/helper.php new file mode 100644 index 00000000000..339bde32326 --- /dev/null +++ b/question/bank/usage/classes/helper.php @@ -0,0 +1,90 @@ +. + +namespace qbank_usage; + +/** + * Helper class for usage. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * Get the usage count for a question. + * + * @param \question_definition $question + * @return int + */ + public static function get_question_entry_usage_count($question) { + global $DB; + + $sql = 'SELECT COUNT(quizid) FROM (' . self::question_usage_sql() . ') AS quizid'; + + return $DB->count_records_sql($sql, [$question->id, $question->id]); + } + + /** + * Get the sql for usage data. + * + * @return string + */ + public static function question_usage_sql(): string { + $sqlset = "(SELECT qz.id as quizid, + qz.name as modulename, + qz.course as courseid + FROM {quiz} as qz + JOIN {quiz_attempts} qa ON qa.quiz = qz.id + JOIN {question_usages} qu ON qu.id = qa.uniqueid + JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id + JOIN {question} q ON q.id = qatt.questionid + WHERE qa.preview = 0 + AND q.id = ?) + UNION + (SELECT qz.id as quizid, + qz.name as modulename, + qz.course as courseid + FROM {quiz_slots} slot + JOIN {quiz} qz ON qz.id = slot.quizid + WHERE slot.questionid = ?)"; + return $sqlset; + } + + /** + * Get question attempt count for the question. + * + * @param int $questionid + * @param int $quizid + * @return int + */ + public static function get_question_attempts_count_in_quiz(int $questionid, int $quizid): int { + global $DB; + $sql = 'SELECT COUNT(qatt.id) + FROM {quiz} as qz + JOIN {quiz_attempts} qa ON qa.quiz = qz.id + JOIN {question_usages} qu ON qu.id = qa.uniqueid + JOIN {question_attempts} qatt ON qatt.questionusageid = qu.id + JOIN {question} q ON q.id = qatt.questionid + WHERE qatt.questionid = :questionid + AND qa.preview = 0 + AND qz.id = :quizid'; + return $DB->count_records_sql($sql, [ 'questionid' => $questionid, 'quizid' => $quizid]); + } + +} diff --git a/question/bank/usage/classes/output/renderer.php b/question/bank/usage/classes/output/renderer.php new file mode 100644 index 00000000000..58c511b465f --- /dev/null +++ b/question/bank/usage/classes/output/renderer.php @@ -0,0 +1,39 @@ +. + +namespace qbank_usage\output; + +/** + * Class renderer + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \plugin_renderer_base { + + /** + * Render the html fragment for usage modal. + * + * @param array $displaydata + * @return string + */ + public function render_usage_fragment(array $displaydata): string { + return $this->render_from_template('qbank_usage/usage_modal', $displaydata); + } + +} diff --git a/question/bank/usage/classes/plugin_feature.php b/question/bank/usage/classes/plugin_feature.php new file mode 100644 index 00000000000..21a39ce9967 --- /dev/null +++ b/question/bank/usage/classes/plugin_feature.php @@ -0,0 +1,34 @@ +. + +namespace qbank_usage; + +/** + * Class plugin_feature is the entrypoint for the columns. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugin_feature extends \core_question\local\bank\plugin_features_base { + + public function get_question_columns($qbank): array { + return [ + new question_usage_column($qbank) + ]; + } +} diff --git a/question/bank/usage/classes/privacy/provider.php b/question/bank/usage/classes/privacy/provider.php new file mode 100644 index 00000000000..0b5b920068a --- /dev/null +++ b/question/bank/usage/classes/privacy/provider.php @@ -0,0 +1,31 @@ +. + +namespace qbank_usage\privacy; + +/** + * Privacy Subsystem for qbank_usage implementing null_provider. + * + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/question/bank/usage/classes/question_usage_column.php b/question/bank/usage/classes/question_usage_column.php new file mode 100644 index 00000000000..cae0d56741c --- /dev/null +++ b/question/bank/usage/classes/question_usage_column.php @@ -0,0 +1,57 @@ +. + +namespace qbank_usage; + +use core_question\local\bank\column_base; + +/** + * A column type for the name of the question type. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_usage_column extends column_base { + + public function get_name(): string { + return 'questionusage'; + } + + protected function get_title(): string { + return get_string('questionusage', 'qbank_usage'); + } + + protected function display_content($question, $rowclasses): void { + global $PAGE; + $usagecount = helper::get_question_entry_usage_count($question); + $attributes = []; + if (question_has_capability_on($question, 'view')) { + $target = 'questionusagepreview_' . $question->id; + $datatarget = '[data-target="' . $target . '"]'; + $PAGE->requires->js_call_amd('qbank_usage/usage', 'init', [$datatarget, $question->contextid]); + $attributes = [ + 'data-target' => $target, + 'data-questionid' => $question->id, + 'data-courseid' => $this->qbank->course->id, + 'class' => 'link-primary comment-pointer' + ]; + } + echo \html_writer::tag('a', $usagecount, $attributes); + } + +} diff --git a/question/bank/usage/classes/tables/question_usage_table.php b/question/bank/usage/classes/tables/question_usage_table.php new file mode 100644 index 00000000000..c3ff5e995ac --- /dev/null +++ b/question/bank/usage/classes/tables/question_usage_table.php @@ -0,0 +1,126 @@ +. + +namespace qbank_usage\tables; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir.'/tablelib.php'); + +use moodle_url; +use qbank_usage\helper; +use table_sql; + +/** + * Class question_usage_table. + * An extension of regular Moodle table. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class question_usage_table extends table_sql { + + /** + * Search string. + * + * @var string $search + */ + public $search = ''; + + /** + * Question id. + * + * @var \question_definition $question + */ + public $question; + + /** + * constructor. + * Sets the SQL for the table and the pagination. + * + * @param string $uniqueid + * @param \question_definition $question + */ + public function __construct(string $uniqueid, \question_definition $question) { + global $PAGE; + parent::__construct($uniqueid); + $this->question = $question; + $columns = ['modulename', 'coursename', 'attempts']; + $headers = [ + get_string('modulename', 'qbank_usage'), + get_string('coursename', 'qbank_usage'), + get_string('attempts', 'qbank_usage') + ]; + $this->is_collapsible = false; + $this->no_sorting('modulename'); + $this->no_sorting('coursename'); + $this->no_sorting('attempts'); + $this->define_columns($columns); + $this->define_headers($headers); + $this->define_baseurl($PAGE->url); + } + + public function query_db($pagesize, $useinitialsbar = true) { + global $DB; + if (!$this->is_downloading()) { + $total = helper::get_question_entry_usage_count($this->question); + $this->pagesize($pagesize, $total); + } + + $sql = helper::question_usage_sql(); + $params = [$this->question->id, $this->question->id]; + + if (!$this->is_downloading()) { + $this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size()); + } else { + $this->rawdata = $DB->get_records_sql($sql, $params); + } + } + + public function col_modulename(\stdClass $values): string { + $params = [ + 'href' => new moodle_url('/mod/quiz/view.php', ['q' => $values->quizid]) + ]; + return \html_writer::tag('a', $values->modulename, $params); + } + + public function col_coursename(\stdClass $values): string { + $course = get_course($values->courseid); + $params = [ + 'href' => new moodle_url('/course/view.php', ['id' => $values->courseid]) + ]; + return \html_writer::tag('a', $course->fullname, $params); + } + + public function col_attempts(\stdClass $values): string { + return helper::get_question_attempts_count_in_quiz($this->question->id, $values->quizid); + } + + /** + * Export this data so it can be used as the context for a mustache template/fragment. + * + * @return string + */ + public function export_for_fragment(): string { + ob_start(); + $this->out(10, true); + return ob_get_clean(); + } + +} diff --git a/question/bank/usage/lang/en/qbank_usage.php b/question/bank/usage/lang/en/qbank_usage.php new file mode 100644 index 00000000000..9c914419966 --- /dev/null +++ b/question/bank/usage/lang/en/qbank_usage.php @@ -0,0 +1,37 @@ +. + +/** + * Strings for component qbank_usage, language 'en' + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Question usage'; +$string['privacy:metadata'] = 'Question usage plugin does not store any user data.'; +$string['questionusage'] = 'Usage'; +$string['usageheader'] = 'Question usage'; + +// Table. +$string['modulename'] = 'Activity name'; +$string['coursename'] = 'Course name'; +$string['versions'] = 'Version'; +$string['state'] = 'State'; +$string['attempts'] = 'Attempts'; +$string['questionusageversion'] = 'v{$a}'; diff --git a/question/bank/usage/lib.php b/question/bank/usage/lib.php new file mode 100644 index 00000000000..6483ee67fce --- /dev/null +++ b/question/bank/usage/lib.php @@ -0,0 +1,63 @@ +. + +/** + * Helper functions and callbacks. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Question usage fragment callback. + * + * @param array $args + * @return string rendered output + */ +function qbank_usage_output_fragment_question_usage(array $args): string { + global $USER, $PAGE, $CFG; + require_once($CFG->dirroot . '/question/engine/bank.php'); + $displaydata = []; + + $question = question_bank::load_question($args['questionid']); + $quba = question_engine::make_questions_usage_by_activity('core_question_preview', context_user::instance($USER->id)); + + $options = new \qbank_previewquestion\question_preview_options($question); + $options->load_user_defaults(); + $options->set_from_request(); + $quba->set_preferred_behaviour($options->behaviour); + $slot = $quba->add_question($question, $options->maxmark); + $quba->start_question($slot, $options->variant); + $displaydata['question'] = $quba->render_question($slot, $options, '1'); + + $questionusagetable = new \qbank_usage\tables\question_usage_table('question_usage_table', $question); + $questionusagetable->baseurl = new moodle_url(''); + if (isset($args['querystring'])) { + $querystring = preg_replace('/^\?/', '', $args['querystring']); + $params = []; + parse_str($querystring, $params); + if (isset($params['page'])) { + $questionusagetable->currpage = $params['page']; + } + } + $displaydata['tablesql'] = $questionusagetable->export_for_fragment(); + + return $PAGE->get_renderer('qbank_usage')->render_usage_fragment($displaydata); +} diff --git a/question/bank/usage/styles.css b/question/bank/usage/styles.css new file mode 100644 index 00000000000..1e9399a1e76 --- /dev/null +++ b/question/bank/usage/styles.css @@ -0,0 +1,6 @@ +.questionusage { + cursor: pointer; +} +#categoryquestions .questionusage { + width: 5em; +} diff --git a/question/bank/usage/templates/usage_modal.mustache b/question/bank/usage/templates/usage_modal.mustache new file mode 100644 index 00000000000..af11f9858a3 --- /dev/null +++ b/question/bank/usage/templates/usage_modal.mustache @@ -0,0 +1,36 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qbank_usage/usage_modal + + Example context (json): + { + "usagedata": [ + { + "question": "question html" + } + ] + } +}} + +
+ {{{question}}} +
+
+ {{{tablesql}}} +
+ diff --git a/question/bank/usage/tests/behat/behat_qbank_usage.php b/question/bank/usage/tests/behat/behat_qbank_usage.php new file mode 100644 index 00000000000..053835cf0ff --- /dev/null +++ b/question/bank/usage/tests/behat/behat_qbank_usage.php @@ -0,0 +1,59 @@ +. + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); +require_once(__DIR__ . '/../../../../tests/behat/behat_question_base.php'); + +use Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; + +/** + * Steps definitions to deal with the usage in question. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_qbank_usage extends behat_question_base { + + /** + * Looks for a table, then looks for a row that contains the given text. + * Once it finds the right row, it clicks a link in that row. + * + * @When I click :arg1 on the usage column + * @param string $linkname + */ + public function i_click_on_the_usage_column($linkname) { + $exception = new ElementNotFoundException($this->getSession(), + 'Cannot find any row on the page containing the text ' . $linkname); + $row = $this->find('css', sprintf('table tbody tr td.questionusage a:contains("%s")', $linkname), $exception); + $row->click(); + } + + /** + * Looks for the appropriate usage count in the column. + * + * @Then I should see :arg1 on the usage column + * @param string $linkdata + */ + public function i_should_see_on_the_usage_column($linkdata) { + $exception = new ElementNotFoundException($this->getSession(), + 'Cannot find any row with the usage count of ' . $linkdata . ' on the column named Usage'); + $this->find('css', sprintf('table tbody tr td.questionusage a:contains("%s")', $linkdata), $exception); + } +} diff --git a/question/bank/usage/tests/behat/question_usage_column.feature b/question/bank/usage/tests/behat/question_usage_column.feature new file mode 100644 index 00000000000..f16eb317255 --- /dev/null +++ b/question/bank/usage/tests/behat/question_usage_column.feature @@ -0,0 +1,44 @@ +@qbank @qbank_usage +Feature: Use the qbank plugin manager page for question usage + In order to check the plugin behaviour with enable and disable + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "activities" exist: + | activity | name | course | idnumber | + | quiz | Test quiz | C1 | quiz1 | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer the first question | + + Scenario: Enable/disable question usage column from the base view + Given I log in as "admin" + And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I should see "Question usage" + When I click on "Disable" "link" in the "Question usage" "table_row" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + Then I should not see "Usage" + And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I click on "Enable" "link" in the "Question usage" "table_row" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I should see "Usage" + + @javascript + Scenario: Question usage modal should work without any usage data + Given I log in as "admin" + And I am on the "Test quiz" "quiz activity" page + And I navigate to "Question bank > Questions" in current page administration + And I set the field "Select a category" to "Test questions" + And I should see "Test questions" + And I should see "0" on the usage column + When I click "0" on the usage column + Then I should see "Question usage" + And I click on "Close" "button" in the ".modal-dialog" "css_element" + And I should see "0" on the usage column diff --git a/question/bank/usage/tests/helper_test.php b/question/bank/usage/tests/helper_test.php new file mode 100644 index 00000000000..937cb550481 --- /dev/null +++ b/question/bank/usage/tests/helper_test.php @@ -0,0 +1,106 @@ +. + +namespace qbank_usage; + +/** + * Helper test. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \qbank_usage\helper + */ +class qbank_usage_helper_test extends \advanced_testcase { + + /** + * @var \stdClass $quiz + */ + protected $quiz; + + /** + * @var array $questions + */ + protected $questions = []; + + /** + * Test setup. + */ + public function setup(): void { + $this->resetAfterTest(); + $layout = '1,2,0'; + // Make a user to do the quiz. + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + // Make a quiz. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $this->quiz = $quizgenerator->create_instance(['course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]); + + $quizobj = \quiz::create($this->quiz->id, $user->id); + + $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + + $page = 1; + foreach (explode(',', $layout) as $slot) { + if ($slot == 0) { + $page += 1; + continue; + } + + $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + quiz_add_quiz_question($question->id, $this->quiz, $page); + $this->questions [] = $question; + } + + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + \quiz_attempt::create($attempt->id); + } + + /** + * Test question attempt count. + * + * @covers ::get_question_attempts_count_in_quiz + */ + public function test_get_question_attempts_count_in_quiz() { + foreach ($this->questions as $question) { + $questionattemptcount = helper::get_question_attempts_count_in_quiz($question->id, $this->quiz->id); + // Test the attempt count matches the usage count, each question should have one count. + $this->assertEquals(1, $questionattemptcount); + } + } + + /** + * Test test usage data. + * + * @covers ::get_question_entry_usage_count + */ + public function test_get_question_entry_usage_count() { + foreach ($this->questions as $question) { + $count = helper::get_question_entry_usage_count($question); + // Test that the attempt data matches the usage data for the count. + $this->assertEquals(1, $count); + } + } +} diff --git a/question/bank/usage/tests/question_usage_test.php b/question/bank/usage/tests/question_usage_test.php new file mode 100644 index 00000000000..e170d3adbc6 --- /dev/null +++ b/question/bank/usage/tests/question_usage_test.php @@ -0,0 +1,84 @@ +. + +namespace qbank_usage; + +/** + * Tests for the data of question usage from differnet areas like helper or usage table. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \qbank_usage\tables\question_usage_table + * @covers qbank_usage_output_fragment_question_usage + */ +class question_usage_test extends \advanced_testcase { + + /** + * Test question usage data. + */ + public function test_question_usage() { + global $PAGE; + $this->resetAfterTest(true); + $layout = '1,2,0'; + // Make a user to do the quiz. + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + // Make a quiz. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + $quiz = $quizgenerator->create_instance(['course' => $course->id, + 'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]); + + $quizobj = \quiz::create($quiz->id, $user->id); + + $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(); + + $questions = []; + $page = 1; + foreach (explode(',', $layout) as $slot) { + if ($slot == 0) { + $page += 1; + continue; + } + + $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + quiz_add_quiz_question($question->id, $quiz, $page); + $questions [] = $question; + } + + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user->id); + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + quiz_attempt_save_started($quizobj, $quba, $attempt); + $attemptdata = \quiz_attempt::create($attempt->id); + + $this->setAdminUser(); + $PAGE->set_url(new \moodle_url('/')); + foreach ($questions as $question) { + $questionusagetable = qbank_usage_output_fragment_question_usage(['questionid' => $question->id]); + // Test usage table contains the quiz data which was attempted. + $this->assertStringContainsString($quiz->name, $questionusagetable); + + // Test usage table contains the course data where the quiz was attempted. + $this->assertStringContainsString($course->fullname, $questionusagetable); + } + } +} diff --git a/question/bank/usage/version.php b/question/bank/usage/version.php new file mode 100644 index 00000000000..f00de6af678 --- /dev/null +++ b/question/bank/usage/version.php @@ -0,0 +1,31 @@ +. + +/** + * Version information for qbank_usage. + * + * @package qbank_usage + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'qbank_usage'; +$plugin->version = 2021092400; +$plugin->requires = 2021052500; +$plugin->maturity = MATURITY_STABLE;