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;