From e7089be8b3d37c9634d979ddf775b281adc5234b Mon Sep 17 00:00:00 2001 From: hieuvu Date: Tue, 16 Sep 2025 13:21:45 +0700 Subject: [PATCH 1/6] Update logic code to handle new optional param We added two new optional params: courseshortname/questionbankidnumber --- classes/attempt.php | 39 +++- classes/embed_id.php | 43 +++- classes/form/embed_options_form.php | 106 +++++++-- classes/output/embed_iframe.php | 4 +- classes/privacy/provider.php | 28 ++- classes/question_options.php | 3 +- classes/utils.php | 334 +++++++++++++++++----------- lang/en/filter_embedquestion.php | 7 +- lib.php | 16 ++ showquestion.php | 4 +- testhelper.php | 8 +- tests/attempt_test.php | 9 +- tests/provider_test.php | 51 +++++ tests/utils_test.php | 96 +++++++- 14 files changed, 556 insertions(+), 192 deletions(-) create mode 100644 tests/provider_test.php diff --git a/classes/attempt.php b/classes/attempt.php index 037d7af..ecaa05f 100644 --- a/classes/attempt.php +++ b/classes/attempt.php @@ -40,6 +40,11 @@ class attempt { */ protected $user; + /** + * @var \context the context in which the question is belong to. + */ + protected $context; + /** * @var \stdClass the question category we are in. */ @@ -88,18 +93,42 @@ public function __construct(embed_id $embedid, embed_location $embedlocation, $this->embedlocation = $embedlocation; $this->user = $user; $this->options = $options; - $this->category = $this->find_category($embedid->categoryidnumber); + $this->category = $this->find_category($embedid->categoryidnumber, $embedid->courseshortname, + $embedid->questionbankidnumber); } /** * Find the category for a category idnumber, if it exists. * * @param string $categoryidnumber idnumber of the category to use. + * @param string|null $courseshortname the short name of the course, if relevant. + * @param string|null $qbankidnumber the idnumber of the question bank, * @return \stdClass if the category was OK. If not null and problem and problemdetails are set. */ - private function find_category(string $categoryidnumber): ?\stdClass { - $coursecontext = \context_course::instance(utils::get_relevant_courseid($this->embedlocation->context)); - $category = utils::get_category_by_idnumber($coursecontext, $categoryidnumber); + private function find_category(string $categoryidnumber, ?string $courseshortname = null, + ?string $qbankidnumber = null): ?\stdClass { + $cmid = utils::get_qbank_by_idnumber(utils::get_relevant_courseid($this->embedlocation->context), + $courseshortname, $qbankidnumber); + if (!$cmid || $cmid === -1) { + if ($cmid === -1) { + $this->problem = 'invalidquestionbank'; + return null; + } else { + if ($qbankidnumber) { + $this->problem = 'invalidqbankidnumber'; + $this->problemdetails = [ + 'qbankidnumber' => $qbankidnumber, + 'contextname' => $this->embedlocation->context_name_for_errors(), + ]; + } else { + $this->problem = 'invalidquestionbank'; + } + return null; + } + } + $context = \context_module::instance($cmid); + $this->context = $context; + $category = utils::get_category_by_idnumber($context, $categoryidnumber); if (!$category) { $this->problem = 'invalidcategory'; $this->problemdetails = [ @@ -443,7 +472,7 @@ public function render_question(\filter_embedquestion\output\renderer $renderer) $relevantcourseid = utils::get_relevant_courseid($this->embedlocation->context); if (question_has_capability_on($this->current_question(), 'edit')) { $this->options->editquestionparams = - ['returnurl' => $this->embedlocation->pageurl, 'courseid' => $relevantcourseid]; + ['returnurl' => $this->embedlocation->pageurl, 'cmid' => $this->context->instanceid]; } // Show an 'Question bank' action to those with permissions. diff --git a/classes/embed_id.php b/classes/embed_id.php index 731a4f6..3a55c98 100644 --- a/classes/embed_id.php +++ b/classes/embed_id.php @@ -40,15 +40,29 @@ class embed_id { */ public $questionidnumber; + /** + * @var string the course shortname. + */ + public $courseshortname; + /** + * @var string the question bank idnumber. + */ + public $questionbankidnumber; + /** * Simple embed_id constructor. * * @param string $categoryidnumber the category idnumber. * @param string $questionidnumber the question idnumber. + * @param null|string $questionbankidnumber the question bank idnumber, optional. + * @param null|string $courseshortname the course shortname, optional. */ - public function __construct(string $categoryidnumber, string $questionidnumber) { + public function __construct(string $categoryidnumber, string $questionidnumber, + ?string $questionbankidnumber = null, ?string $courseshortname = null) { $this->categoryidnumber = $categoryidnumber; $this->questionidnumber = $questionidnumber; + $this->questionbankidnumber = $questionbankidnumber; + $this->courseshortname = $courseshortname; } /** @@ -61,10 +75,15 @@ public static function create_from_string(string $questioninfo): ?embed_id { if (strpos($questioninfo, '/') === false) { return null; } - - list($categoryidnumber, $questionidnumber) = explode('/', $questioninfo, 2); + $parts = explode('/', $questioninfo); + // Ensure 4 parts, right-aligned. + $parts = array_pad($parts, -4, ''); + // Assign in order: courseshortname, qbankid, categoryid, questionid. + [$courseshortname, $questionbankidnumber, $categoryidnumber, $questionidnumber] = $parts; return new embed_id(str_replace(self::ESCAPED, self::TO_ESCAPE, $categoryidnumber), - str_replace(self::ESCAPED, self::TO_ESCAPE, $questionidnumber)); + str_replace(self::ESCAPED, self::TO_ESCAPE, $questionidnumber), + str_replace(self::ESCAPED, self::TO_ESCAPE, $questionbankidnumber), + str_replace(self::ESCAPED, self::TO_ESCAPE, $courseshortname)); } /** @@ -73,7 +92,13 @@ public static function create_from_string(string $questioninfo): ?embed_id { * @return string categoryidnumber/questionidnumber. */ public function __toString(): string { - return str_replace(self::TO_ESCAPE, self::ESCAPED, $this->categoryidnumber) . '/' . + $optional = !empty($this->courseshortname) ? + str_replace(self::TO_ESCAPE, self::ESCAPED, $this->courseshortname) . '/' : ''; + $optional .= !empty($this->questionbankidnumber) ? + str_replace(self::TO_ESCAPE, self::ESCAPED, $this->questionbankidnumber) . '/' : ''; + + return $optional . + str_replace(self::TO_ESCAPE, self::ESCAPED, $this->categoryidnumber) . '/' . str_replace(self::TO_ESCAPE, self::ESCAPED, $this->questionidnumber); } @@ -84,7 +109,11 @@ public function __toString(): string { * that are safe in HTML id attributes. */ public function to_html_id(): string { - return clean_param($this->categoryidnumber, PARAM_ALPHANUMEXT) . '/' . + $optional = !empty($this->courseshortname) ? clean_param($this->courseshortname, PARAM_ALPHANUMEXT) . '/' : ''; + $optional .= !empty($this->questionbankidnumber) ? clean_param($this->questionbankidnumber, PARAM_ALPHANUMEXT) . '/' : ''; + + return $optional . + clean_param($this->categoryidnumber, PARAM_ALPHANUMEXT) . '/' . clean_param($this->questionidnumber, PARAM_ALPHANUMEXT); } @@ -94,6 +123,8 @@ public function to_html_id(): string { * @param \moodle_url $url the URL to add to. */ public function add_params_to_url(\moodle_url $url): void { + $url->param('courseshortname', $this->courseshortname); + $url->param('questionbankidnumber', $this->questionbankidnumber); $url->param('catid', $this->categoryidnumber); $url->param('qid', $this->questionidnumber); } diff --git a/classes/form/embed_options_form.php b/classes/form/embed_options_form.php index e72ee36..e59fefe 100644 --- a/classes/form/embed_options_form.php +++ b/classes/form/embed_options_form.php @@ -27,9 +27,11 @@ global $CFG; require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->libdir . '/modinfolib.php'); + use filter_embedquestion\utils; use filter_embedquestion\question_options; - +use cm_info; /** * Form to let users edit all the options for embedding a question. @@ -38,36 +40,86 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class embed_options_form extends \moodleform { - #[\Override] public function definition() { - global $PAGE; + global $PAGE, $OUTPUT; $mform = $this->_form; + /** @var \context $context */ + $context = $this->_customdata['context']; + $courseshortname = $this->_customdata['cousrseshortname'] ?? null; + $defaultqbankcmid = $this->_customdata['qbankcmid'] ?? null; + $embedcode = $this->_customdata['embedcode'] ?? null; + // The default form id ('mform1') is also highly likely to be the same as the // id of the form in the background when we are shown in an atto editor pop-up. // Therefore, set something different. $mform->updateAttributes(['id' => 'embedqform']); - /** @var \context $context */ - $context = $this->_customdata['context']; - $defaultoptions = new question_options(); - + $mform->addElement('hidden', 'contextid', $context->id); + $mform->setType('contextid', PARAM_INT); + $mform->setType('courseshortname', PARAM_RAW); $mform->addElement('hidden', 'courseid', $context->instanceid); $mform->setType('courseid', PARAM_INT); - + $mform->addElement('hidden', 'courseshortname', ''); + $mform->setType('courseshortname', PARAM_RAW); $mform->addElement('header', 'questionheader', get_string('whichquestion', 'filter_embedquestion')); + $prefs = []; + + // Only load user preference if we do not have a default question bank cmid or embed code. + if (!$defaultqbankcmid && !$embedcode) { + // Preference key unique to your filter. + $prefname = 'filter_embedquestion_userdefaultqbank'; + // Retrieve existing preference (empty array if none). + $prefs = json_decode(get_user_preferences($prefname, '{}')); + } + + $cmid = !empty($defaultqbankcmid) ? $defaultqbankcmid : ($prefs->{$context->instanceid} ?? null); + // If we have default question bank cmid, we will use it to get the course shortname. + if ($cmid) { + [, $cm] = get_course_and_cm_from_cmid($cmid); + $cminfo = cm_info::create($cm); + $courseshortname = $cminfo->get_course()->shortname; + if ($cminfo->get_course()->id !== $context->instanceid) { + // If the course shortname is not the same as the course shortname, we need to add it to the form. + // This is to allow the course shortname to the embed code. + $mform->setDefault('courseshortname', $courseshortname); + } + } + + $qbanks = utils::get_shareable_question_banks($context->instanceid, $courseshortname, + $this->get_user_retriction()); + $qbanksselectoptions = utils::create_select_qbank_choices($qbanks); + // Build the hidden array of question bank idnumbers. + $qbanksidnumber = array_combine( + array_keys($qbanks), + array_map(fn($q) => $q->qbankidnumber, $qbanks) + ); + // If we have a default question bank cmid, we will use it to set the default value. + // If the default question bank cmid is not in the list of question banks, we will add it. + if ($cmid && empty($qbanksselectoptions[$cmid])) { + $qbanksselectoptions[$cmid] = format_string($cminfo->name); + $qbanksidnumber[$cmid] = $cminfo->idnumber; + } + $mform->addElement('html', $OUTPUT->render_from_template('mod_quiz/switch_bank_header', + ['currentbank' => reset($qbanksselectoptions)])); + $mform->addElement('select', 'qbankcmid', get_string('questionbank', 'question'), + $qbanksselectoptions); + $mform->addRule('qbankcmid', null, 'required', null, 'client'); + $mform->addElement('hidden', 'qbankidnumber'); + $mform->setType('qbankidnumber', PARAM_RAW); + $mform->setDefault('qbankidnumber', json_encode($qbanksidnumber)); + $mform->addElement('select', 'categoryidnumber', get_string('questioncategory', 'question'), - utils::get_categories_with_sharable_question_choices( - $context, $this->get_user_retriction())); + []); $mform->addRule('categoryidnumber', null, 'required', null, 'client'); - + $mform->disabledIf('questionidnumber', 'qbankcmid', 'eq', ''); $mform->addElement('select', 'questionidnumber', get_string('question'), []); $mform->addRule('questionidnumber', null, 'required', null, 'client'); $mform->disabledIf('questionidnumber', 'categoryidnumber', 'eq', ''); - $PAGE->requires->js_call_amd('filter_embedquestion/questionid_choice_updater', 'init'); + $PAGE->requires->js_call_amd('filter_embedquestion/questionid_choice_updater', 'init', [$cmid]); $mform->addElement('text', 'iframedescription', get_string('iframedescription', 'filter_embedquestion'), ['size' => 100]); @@ -164,17 +216,37 @@ protected function get_marks_options(int $default): array { public function definition_after_data() { parent::definition_after_data(); $mform = $this->_form; + $qbankcmid = $mform->getElementValue('qbankcmid'); + if (is_null($qbankcmid)) { + return; + } $categoryidnumbers = $mform->getElementValue('categoryidnumber'); if (is_null($categoryidnumbers)) { return; } + $qbankcmid = $qbankcmid[0]; $categoryidnumber = $categoryidnumbers[0]; if ($categoryidnumber === '' || $categoryidnumber === null) { return; } + $courseshortname = $mform->getElementValue('courseshortname'); + if ($courseshortname) { + $qbanks = utils::get_shareable_question_banks($this->_customdata['context']->instanceid, + $courseshortname, $this->get_user_retriction()); + $qbanksselectoptions = utils::create_select_qbank_choices($qbanks); + $element = $mform->getElement('qbankcmid'); + // Clear the existing options, so that we can load the new ones. + $element->_options = []; + $mform->getElement('qbankcmid')->loadArray($qbanksselectoptions); + } + $context = \context_module::instance($qbankcmid); + $mform->setDefault('qbankcmid', $qbankcmid); - $category = utils::get_category_by_idnumber($this->_customdata['context'], $categoryidnumber); + $categories = utils::get_categories_with_sharable_question_choices($context, + $this->get_user_retriction()); + $mform->getElement('categoryidnumber')->loadArray($categories); + $category = utils::get_category_by_idnumber($context, $categoryidnumber); if ($category) { $choices = utils::get_sharable_question_choices($category->id, $this->get_user_retriction()); $mform->getElement('questionidnumber')->loadArray($choices); @@ -205,9 +277,11 @@ protected function get_user_retriction(): ?int { #[\Override] public function validation($data, $files) { $errors = parent::validation($data, $files); - $context = $this->_customdata['context']; - - $category = utils::get_category_by_idnumber($context, $data['categoryidnumber']); + if (!isset($data['qbankcmid'])) { + $errors['qbankcmid'] = get_string('errorquestionbanknotfound', 'filter_embedquestion'); + } + $qbankcontext = \context_module::instance($data['qbankcmid']); + $category = utils::get_category_by_idnumber($qbankcontext, $data['categoryidnumber']); $questiondata = false; if (isset($data['questionidnumber'])) { diff --git a/classes/output/embed_iframe.php b/classes/output/embed_iframe.php index 0b79d74..c927c9d 100644 --- a/classes/output/embed_iframe.php +++ b/classes/output/embed_iframe.php @@ -51,7 +51,9 @@ public function export_for_template(renderer_base $output): array { 'name' => null, 'iframedescription' => format_string($this->iframedescription), 'embedid' => (new embed_id($this->showquestionurl->param('catid'), - $this->showquestionurl->param('qid')))->to_html_id(), + $this->showquestionurl->param('qid'), + $this->showquestionurl->param('questionbankidnumber'), + $this->showquestionurl->param('courseshortname')))->to_html_id(), ]; if (defined('BEHAT_SITE_RUNNING')) { $data['name'] = 'filter_embedquestion-iframe'; diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 6816c99..106d505 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -16,6 +16,8 @@ namespace filter_embedquestion\privacy; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\writer; /** * Privacy Subsystem for filter_embedquestion implementing null_provider. * @@ -23,15 +25,23 @@ * @copyright 2018 The Open Univesity * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements \core_privacy\local\metadata\null_provider { +class provider implements \core_privacy\local\metadata\provider, + \core_privacy\local\request\user_preference_provider { + #[\Override] + public static function get_metadata(collection $collection): collection { + $collection->add_user_preference('filter_embedquestion_userdefaultqbank', 'privacy:preference:defaultqbank'); + } - /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. - * - * @return string The language string identifier with the component's language. - */ - public static function get_reason(): string { - return 'privacy:metadata'; + #[\Override] + public static function export_user_preferences(int $userid): void { + $defaultqbanks = get_user_preferences('filter_embedquestion_userdefaultqbank', '{}', $userid); + if ($defaultqbanks !== '{}') { + writer::export_user_preference( + 'filter_embedquestion', + 'userdefaultqbank', + $defaultqbanks, + get_string('defaultqbank', 'filter_embedquestion') + ); + } } } diff --git a/classes/question_options.php b/classes/question_options.php index 9c7b812..892f2a9 100644 --- a/classes/question_options.php +++ b/classes/question_options.php @@ -176,7 +176,8 @@ public function add_params_to_url(\moodle_url $url): void { */ public static function get_embed_from_form_options(\stdClass $fromform): string { - $embedid = new embed_id($fromform->categoryidnumber, $fromform->questionidnumber); + $embedid = new embed_id($fromform->categoryidnumber, $fromform->questionidnumber, + $fromform->questionbankidnumber ?? '', $fromform->courseshortname); $parts = [(string) $embedid]; foreach (self::get_field_types() as $field => $type) { if (!isset($fromform->$field) || $fromform->$field === '') { diff --git a/classes/utils.php b/classes/utils.php index 05e4439..27e6abd 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -137,6 +137,51 @@ public static function get_show_url(embed_id $embedid, embed_location $embedloca return $url; } + /** + * Find a question bank with a given idnumber in a given course. + * + * @param int $currentcourseid the id of the course to look in. + * @param string|null $courseshortname the shortname of the course to look in. + * @param string|null $qbankidnumber the idnumber of the question bank to look for. + * @param int|null $userid if set, only count question banks created by this user. + * @return int|null cmid or null if not found. + * If there are multiple question banks in the course, and no idnumber is given, return -1 only if there is no + * question bank with no idnumber created by system. + */ + public static function get_qbank_by_idnumber(int $currentcourseid, ?string $courseshortname = null, + ?string $qbankidnumber = null, ?int $userid = null): ?int { + $qbanks = self::get_shareable_question_banks($currentcourseid, $courseshortname, $userid, $qbankidnumber); + if (empty($qbanks)) { + return null; + } else if (count($qbanks) === 1) { + $cmid = reset($qbanks)->cmid; + } else { + if (!$qbankidnumber || $qbankidnumber === '*') { + // Multiple qbanks in this course. + $qbankswithoutidnumber = array_filter($qbanks, function($qbank) { + return empty($qbank->qbankidnumber); + }); + if (count($qbankswithoutidnumber) === 1) { + $cmid = reset($qbankswithoutidnumber)->cmid; + } else { + // There are multiple question banks without id number and we can't determine which one to use. + return -1; + } + } else { + // We have a qbankidnumber, so we can filter the list. + $match = array_filter($qbanks, fn($q) => $q->qbankidnumber === $qbankidnumber); + if (count($match) === 1) { + $cmid = reset($match)->cmid; + } else { + // There are multiple question banks with id number and we can't determine which one to use. + return -1; + } + } + } + + return $cmid; + } + /** * Find a category with a given idnumber in a given context. * @@ -156,6 +201,91 @@ public static function get_category_by_idnumber(\context $context, string $idnum return $category; } + /** + * Get a list of the question banks that have sharable questions in the specific course. + * + * The list is returned in a form suitable for using in a select menu. + * + * @param int $currentcourseid the id of the course to look in. + * @param string|null $courseshortname the shortname of the course to look in. + * @param int|null $userid if set, only count question banks created by this user. + * @param string|null $qbankidnumber if set, only count question banks with this idnumber. + * @return array course module id => object with fields cmid, qbankidnumber, courseid, qbankid. + */ + public static function get_shareable_question_banks(int $currentcourseid, ?string $courseshortname = null, + ?int $userid = null, ?string $qbankidnumber = null): array { + global $DB; + $params = [ + 'modulename' => 'qbank', + 'courseshortname' => $courseshortname ?: null, + 'currentcourseid' => $courseshortname ? null : $currentcourseid, + 'contextlevel' => CONTEXT_MODULE, + 'ready' => question_version_status::QUESTION_STATUS_READY, + ]; + + $creatortest = ''; + if ($userid) { + $creatortest = 'AND qbe.ownerid = :userid'; + $params['userid'] = $userid; + } + + $idnumber = ''; + if ($qbankidnumber && $qbankidnumber !== '*') { + $idnumber = 'AND cm.idnumber = :qbankidnumber'; + $params['qbankidnumber'] = $qbankidnumber; + } + + $sql = "SELECT cm.id AS cmid, + cm.idnumber AS qbankidnumber, + qbank.id AS qbankid, + qbank.name, + qbank.type + FROM {course} c + JOIN {course_modules} cm ON cm.course = c.id + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {qbank} qbank ON qbank.id = cm.instance + JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel + JOIN {question_categories} qc ON qc.contextid = ctx.id + JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id + AND qbe.idnumber IS NOT NULL + $creatortest + JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id + AND qv.version = (SELECT MAX(qv2.version) + FROM {question_versions} qv2 + WHERE qv2.questionbankentryid = qbe.id + AND qv2.status = :ready + ) + JOIN {question} q ON q.id = qv.questionid + WHERE (c.shortname = :courseshortname OR c.id = :currentcourseid) + AND qc.idnumber IS NOT NULL + AND qc.idnumber <> '' + $idnumber + GROUP BY cm.id, cm.idnumber, qbank.id, qbank.name + HAVING COUNT(q.id) > 0 + ORDER BY cm.id"; + $qbanks = $DB->get_records_sql($sql, $params); + return $qbanks; + } + + /** + * Create a list of question banks in a form suitable for using in a select menu. + * + * @param array $qbanks the question banks, as returned by {@see get_shareable_question_banks()}. + * @return array course module id => question bank name (and idnumber if set). + */ + public static function create_select_qbank_choices(array $qbanks): array { + $choices = ['' => get_string('choosedots')]; + foreach ($qbanks as $cmid => $qbank) { + if ($qbank->qbankidnumber) { + $choices[$cmid] = get_string('nameandidnumber', 'filter_embedquestion', + ['name' => format_string($qbank->name), 'idnumber' => s($qbank->qbankidnumber)]); + } else { + $choices[$cmid] = format_string($qbank->name); + } + } + return $choices; + } + /** * Find a question with a given idnumber in a given context. * @@ -166,29 +296,22 @@ public static function get_category_by_idnumber(\context $context, string $idnum public static function get_question_by_idnumber(int $categoryid, string $idnumber): ?\stdClass { global $DB; - if (self::has_question_versionning()) { - $question = $DB->get_record_sql(' - SELECT q.*, qbe.idnumber, qbe.questioncategoryid AS category, - qv.id AS versionid, qv.version, qv.questionbankentryid - FROM {question_bank_entries} qbe - JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( - SELECT MAX(version) - FROM {question_versions} - WHERE questionbankentryid = qbe.id AND status = :ready - ) - JOIN {question} q ON q.id = qv.questionid - WHERE qbe.questioncategoryid = :category AND qbe.idnumber = :idnumber', - [ - 'ready' => question_version_status::QUESTION_STATUS_READY, - 'category' => $categoryid, 'idnumber' => $idnumber, - ], - ); - } else { - $question = $DB->get_record_select('question', - "category = ? AND idnumber = ? AND hidden = 0 AND parent = 0", - [$categoryid, $idnumber]); - - } + $question = $DB->get_record_sql(' + SELECT q.*, qbe.idnumber, qbe.questioncategoryid AS category, + qv.id AS versionid, qv.version, qv.questionbankentryid + FROM {question_bank_entries} qbe + JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( + SELECT MAX(version) + FROM {question_versions} + WHERE questionbankentryid = qbe.id AND status = :ready + ) + JOIN {question} q ON q.id = qv.questionid + WHERE qbe.questioncategoryid = :category AND qbe.idnumber = :idnumber', + [ + 'ready' => question_version_status::QUESTION_STATUS_READY, + 'category' => $categoryid, 'idnumber' => $idnumber, + ], + ); if (!$question) { return null; } @@ -235,65 +358,42 @@ public static function is_latest_version(\question_definition $question): bool { public static function get_categories_with_sharable_question_choices(\context $context, int|null $userid = null): array { global $DB; + $params = []; + $creatortest = ''; + if ($userid) { + $creatortest = 'AND qbe.ownerid = :userid'; + $params['userid'] = $userid; + } + $params['status'] = question_version_status::QUESTION_STATUS_READY; + $params['cmid'] = $context->instanceid; + $params['contextlevel'] = CONTEXT_MODULE; + $params['modulename'] = 'qbank'; + + $categories = $DB->get_records_sql(" + SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count + + FROM {question_categories} qc + JOIN {context} ctx ON ctx.id = qc.contextid + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {qbank} qbank ON qbank.id = cm.instance + JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id + AND qbe.idnumber IS NOT NULL $creatortest + JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( + SELECT MAX(version) + FROM {question_versions} + WHERE questionbankentryid = qbe.id AND status = :status + ) + JOIN {question} q ON q.id = qv.questionid - if (self::has_question_versionning()) { - $params = []; - $creatortest = ''; - if ($userid) { - $creatortest = 'AND qbe.ownerid = ?'; - $params[] = $userid; - } - $params[] = question_version_status::QUESTION_STATUS_READY; - $params[] = $context->id; - - $categories = $DB->get_records_sql(" - SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count - - FROM {question_categories} qc - JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id - AND qbe.idnumber IS NOT NULL $creatortest - JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( - SELECT MAX(version) - FROM {question_versions} - WHERE questionbankentryid = qbe.id AND status = ? - ) - JOIN {question} q ON q.id = qv.questionid - - WHERE qc.contextid = ? - AND qc.idnumber IS NOT NULL - - GROUP BY qc.id, qc.name, qc.idnumber - HAVING COUNT(q.id) > 0 - ORDER BY qc.name - ", $params); - - } else { - $params = []; - $creatortest = ''; - if ($userid) { - $creatortest = 'AND q.createdby = ?'; - $params[] = $userid; - } - $params[] = $context->id; - - $categories = $DB->get_records_sql(" - SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count - - FROM {question_categories} qc - JOIN {question} q ON q.category = qc.id - AND q.idnumber IS NOT NULL - $creatortest - AND q.hidden = 0 - AND q.parent = 0 - - WHERE qc.contextid = ? + WHERE cm.id = :cmid + AND ctx.contextlevel = :contextlevel AND qc.idnumber IS NOT NULL - - GROUP BY qc.id, qc.name, qc.idnumber - HAVING COUNT(q.id) > 0 - ORDER BY qc.name - ", $params); - } + AND qc.idnumber <> '' + GROUP BY qc.id, qc.name, qc.idnumber + HAVING COUNT(q.id) > 0 + ORDER BY qc.name + ", $params); $choices = ['' => get_string('choosedots')]; foreach ($categories as $category) { @@ -316,57 +416,32 @@ public static function get_categories_with_sharable_question_choices(\context $c public static function get_sharable_question_ids(int $categoryid, int|null $userid = null): array { global $DB; - if (self::has_question_versionning()) { - $params = []; - $params[] = question_version_status::QUESTION_STATUS_READY; - $params[] = $categoryid; - $creatortest = ''; - if ($userid) { - $creatortest = 'AND qbe.ownerid = ?'; - $params[] = $userid; - } - - return $DB->get_records_sql(" - SELECT q.id, q.name, qbe.idnumber - - FROM {question_bank_entries} qbe - JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( - SELECT MAX(version) - FROM {question_versions} - WHERE questionbankentryid = qbe.id AND status = ? - ) - JOIN {question} q ON q.id = qv.questionid - - WHERE qbe.questioncategoryid = ? - AND qbe.idnumber IS NOT NULL - $creatortest - - ORDER BY q.name - ", $params); - - } else { - $params = []; - $params[] = $categoryid; - $creatortest = ''; - if ($userid) { - $creatortest = 'AND q.createdby = ?'; - $params[] = $userid; - } + $params = []; + $params[] = question_version_status::QUESTION_STATUS_READY; + $params[] = $categoryid; + $creatortest = ''; + if ($userid) { + $creatortest = 'AND qbe.ownerid = ?'; + $params[] = $userid; + } - return $DB->get_records_sql(" - SELECT q.id, q.name, q.idnumber + return $DB->get_records_sql(" + SELECT q.id, q.name, qbe.idnumber - FROM {question} q + FROM {question_bank_entries} qbe + JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( + SELECT MAX(version) + FROM {question_versions} + WHERE questionbankentryid = qbe.id AND status = ? + ) + JOIN {question} q ON q.id = qv.questionid - WHERE q.category = ? - AND q.idnumber IS NOT NULL - $creatortest - AND q.hidden = 0 - AND q.parent = 0 + WHERE qbe.questioncategoryid = ? + AND qbe.idnumber IS NOT NULL + $creatortest - ORDER BY q.name - ", $params); - } + ORDER BY q.name + ", $params); } /** @@ -456,9 +531,6 @@ public static function get_question_bank_url(\question_definition $question): \m require_once($CFG->dirroot . '/question/editlib.php'); $context = \context::instance_by_id($question->contextid); - if ($context->contextlevel != CONTEXT_COURSE) { - throw new \coding_exception('Unexpected. Only questions from the course question bank should be embedded.'); - } $latestquestionid = $DB->get_field_sql(" SELECT qv.questionid @@ -472,7 +544,7 @@ public static function get_question_bank_url(\question_definition $question): \m ", [$question->questionbankentryid, $question->questionbankentryid]); return new \moodle_url('/question/edit.php', [ - 'courseid' => $context->instanceid, + 'cmid' => $context->instanceid, 'cat' => $question->category . ',' . $question->contextid, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE, 'lastchanged' => $latestquestionid, diff --git a/lang/en/filter_embedquestion.php b/lang/en/filter_embedquestion.php index 5c6765a..e78aa33 100644 --- a/lang/en/filter_embedquestion.php +++ b/lang/en/filter_embedquestion.php @@ -31,6 +31,7 @@ $string['chooserandomly'] = 'Choose an embeddable question from this category randomly'; $string['corruptattempt'] = 'Your previous attempt at a question here has stopped working. If you click continue, it will be removed and a new attempt created.'; $string['corruptattemptwithreason'] = 'Your previous attempt at a question here has stopped working. ({$a}) If you click continue, it will be removed and a new attempt created.'; +$string['defaultqbank'] = 'Default question bank for each course'; $string['defaultsheading'] = 'Default options for embedding questions'; $string['defaultsheading_desc'] = 'These are the defaults for the options that control how embedded questions display and function. These are the values that will be used if a particular option is not set when the question is embedded.'; $string['defaultx'] = 'Default ({$a})'; @@ -38,6 +39,7 @@ $string['embedquestion'] = 'Embed question'; $string['errormaxmarknumber'] = 'The maximum mark must be a number.'; $string['errornopermissions'] = 'You do not have permission to embed this question.'; +$string['errorquestionbanknotfound'] = 'The question bank does not exist'; $string['errorunknownquestion'] = 'Unknown, or unsharable question.'; $string['errorvariantformat'] = 'Variant number must be a positive integer.'; $string['errorvariantoutofrange'] = 'Variant number must be a positive integer at most {$a}.'; @@ -53,9 +55,11 @@ $string['iframedescriptionminlengthwarning'] = 'A description must have at least three characters.'; $string['iframetitle'] = 'Embedded question'; $string['iframetitleauto'] = 'Embedded question {$a}'; +$string['invalidcantfindquestionbank'] = 'We have multiple question banks in this context "{$a->contextname}", but the one you are trying to use does not exist.'; $string['invalidcategory'] = 'The category with idnumber "{$a->catid}" does not exist in "{$a->contextname}".'; -$string['invalidemptycategory'] = 'The category "{$a->catname}" in "{$a->contextname}" does not contain any embeddable questions.'; +$string['invalidqbankidnumber'] = 'The question bank with idnumber "{$a->qbankidnumber}" does not exist in "{$a->contextname}".'; $string['invalidquestion'] = 'The question with idnumber "{$a->qid}" does not exist in category "{$a->catname} [{$a->catidnumber}]".'; +$string['invalidquestionbank'] = 'The question bank does not exist.'; $string['invalidrandomquestion'] = 'Cannot generate a random question from the question category "{$a}".'; $string['invalidtoken'] = 'This embedded question is incorrectly configured.'; $string['markdp_desc'] = 'The default number of digits that should be shown after the decimal point when displaying grades in embedded questions.'; @@ -69,6 +73,7 @@ $string['pluginname'] = 'Embed questions'; $string['previousattempts'] = 'Previous attempts'; $string['privacy:metadata'] = 'The Embed questions filter does not store any personal data.'; +$string['privacy:preference:defaultqbank'] = 'Stores default qbank mapping from course ID to selected question bank (JSON-encoded).'; $string['questionbank'] = 'Question bank'; $string['questionidnumber'] = 'Question id number'; $string['questionidnumberchanged'] = 'The question being attempted here no longer has idnumber {$a}.'; diff --git a/lib.php b/lib.php index bd5a83a..53a88f5 100644 --- a/lib.php +++ b/lib.php @@ -73,3 +73,19 @@ function filter_embedquestion_question_pluginfile($givencourse, $context, $compo send_stored_file($file, 0, 0, $forcedownload, $fileoptions); } + +/** + * Allow update of user preferences via AJAX. + * + * @return array[] + */ +function filter_embedquestion_user_preferences(): array { + return [ + 'filter_embedquestion_userdefaultqbank' => [ + 'type' => PARAM_RAW, + 'null' => NULL_NOT_ALLOWED, + 'default' => '{}', + 'permissioncallback' => [core_user::class, 'is_current_user'], + ], + ]; +} diff --git a/showquestion.php b/showquestion.php index 69bec35..c246218 100644 --- a/showquestion.php +++ b/showquestion.php @@ -56,7 +56,9 @@ // Process other parameters. $categoryidnumber = required_param('catid', PARAM_RAW); $questionidnumber = required_param('qid', PARAM_RAW); -$embedid = new embed_id($categoryidnumber, $questionidnumber); +$questionbankidnumber = optional_param('questionbankidnumber', '', PARAM_RAW); +$courseshortname = optional_param('courseshortname', '', PARAM_RAW); +$embedid = new embed_id($categoryidnumber, $questionidnumber, $questionbankidnumber, $courseshortname); $embedlocation = embed_location::make_from_url_params(); diff --git a/testhelper.php b/testhelper.php index 7132610..644fafd 100644 --- a/testhelper.php +++ b/testhelper.php @@ -50,13 +50,14 @@ utils::warn_if_filter_disabled($context); if ($fromform = $form->get_data()) { - $category = utils::get_category_by_idnumber($context, $fromform->categoryidnumber); + $qbankcontext = context_module::instance($fromform->qbankcmid); + $category = utils::get_category_by_idnumber($qbankcontext, $fromform->categoryidnumber); if ($fromform->questionidnumber === '*') { echo $OUTPUT->heading('Information for embedding question selected randomly from ' . format_string($category->name)); \filter_embedquestion\event\category_token_created::create( - ['context' => $context, 'objectid' => $category->id])->trigger(); + ['context' => $qbankcontext, 'objectid' => $category->id])->trigger(); } else { $questiondata = utils::get_question_by_idnumber($category->id, $fromform->questionidnumber); @@ -67,7 +68,8 @@ \filter_embedquestion\event\token_created::create( ['context' => $context, 'objectid' => $question->id])->trigger(); } - + $fromform->questionbankidnumber = ''; + $fromform->courseshortname = ''; $embedcode = question_options::get_embed_from_form_options($fromform); echo html_writer::tag('p', 'Code to embed the question: ' . s($embedcode)); diff --git a/tests/attempt_test.php b/tests/attempt_test.php index f797aa6..470a4b5 100644 --- a/tests/attempt_test.php +++ b/tests/attempt_test.php @@ -277,10 +277,7 @@ public function test_question_rendering(): void { $previousattemptlink = ''; } - $icon = ']*>Edit question'; - if (utils::moodle_version_is("<=", "44")) { - $icon = ']*>Edit question'; - } + $icon = ']*>\s*Edit question\s*\s*'; // Verify that the edit question, question bank link and fill with correct links are present. $expectedregex = '~

Question [^<]+' . @@ -294,8 +291,8 @@ public function test_question_rendering(): void { 'Question bank

' . '~'; + '' . + 'Fill with correct~s'; $this->assertMatchesRegularExpression($expectedregex, $html); // Create an authenticated user. $user = $this->getDataGenerator()->create_user(); diff --git a/tests/provider_test.php b/tests/provider_test.php new file mode 100644 index 0000000..ead7384 --- /dev/null +++ b/tests/provider_test.php @@ -0,0 +1,51 @@ +. + +namespace filter_embedquestion; + +use advanced_testcase; +use core_privacy\local\request\writer; +use filter_embedquestion\privacy\provider; + +/** + * Unit tests for filter_embedquestion privacy provider. + * + * @package filter_embedquestion + * @copyright 2025 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \filter_embedquestion\privacy\provider + */ +final class provider_test extends advanced_testcase { + /** + * Test to check export_user_preferences. + * + * @covers ::export_user_preferences + */ + public function test_export_user_preferences(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Simulate saved user preference JSON. + $example = ['1' => '2', '2' => '3']; + set_user_preference('filter_embedquestion_userdefaultqbank', json_encode($example), $user->id); + provider::export_user_preferences($user->id); + $writer = writer::with_context(\context_system::instance()); + $prefs = $writer->get_user_preferences('filter_embedquestion'); + $this->assertEquals(get_string('defaultqbank', 'filter_embedquestion'), $prefs->userdefaultqbank->description); + $this->assertEquals(json_encode($example), $prefs->userdefaultqbank->value); + } +} diff --git a/tests/utils_test.php b/tests/utils_test.php index aeaa07f..bf9da24 100644 --- a/tests/utils_test.php +++ b/tests/utils_test.php @@ -41,10 +41,11 @@ public function test_get_category_by_idnumber(): void { $catwithidnumber = $questiongenerator->create_question_category( ['name' => 'Category with idnumber', 'idnumber' => 'abc123']); $questiongenerator->create_question_category(); + $context = \context::instance_by_id($catwithidnumber->contextid); $this->assertEquals($catwithidnumber->id, utils::get_category_by_idnumber( - \context_system::instance(), 'abc123')->id); + $context, 'abc123')->id); } public function test_get_category_by_idnumber_not_existing(): void { @@ -97,10 +98,11 @@ public function test_get_categories_with_sharable_question_choices(): void { $questiongenerator->create_question('shortanswer', null, ['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']); + $context = \context::instance_by_id($catwithid2->contextid); $this->assertEquals( ['' => 'Choose...', 'pqr789' => 'Second category [pqr789] (1)'], - utils::get_categories_with_sharable_question_choices(\context_system::instance())); + utils::get_categories_with_sharable_question_choices($context)); } public function test_get_categories_with_sharable_question_choices_only_user(): void { @@ -122,11 +124,11 @@ public function test_get_categories_with_sharable_question_choices_only_user(): $questiongenerator->create_question('shortanswer', null, ['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']); $this->setAdminUser(); - + $context = \context::instance_by_id($catwithid1->contextid); $this->assertEquals([ '' => 'Choose...', 'abc123' => 'Category with idnumber [abc123] (1)', - ], utils::get_categories_with_sharable_question_choices(\context_system::instance(), $USER->id)); + ], utils::get_categories_with_sharable_question_choices($context, $USER->id)); } public function test_get_sharable_question_choices(): void { @@ -234,12 +236,13 @@ public function test_get_categories_with_sharable_question_choices_should_not_in $questiongenerator->create_question('shortanswer', null, ['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']); + $context = \context::instance_by_id($catwithid2->contextid); // The random question should not appear in the counts. $this->assertEquals([ '' => 'Choose...', 'pqr789' => 'Second category with [pqr789] (1)', - ], utils::get_categories_with_sharable_question_choices(\context_system::instance())); + ], utils::get_categories_with_sharable_question_choices($context)); } /** @@ -299,17 +302,17 @@ public function test_get_categories_with_sharable_question_choices_should_not_in $catwithid2 = $questiongenerator->create_question_category( ['name' => 'Second category with', 'idnumber' => 'pqr789']); $questiongenerator->create_question_category(); - $questiongenerator->create_question('shortanswer', null, ['category' => $catwithid2->id, 'name' => 'Question', 'idnumber' => 'frog']); $this->create_hidden_question('shortanswer', null, ['category' => $catwithid2->id, 'name' => 'Question (hidden)', 'idnumber' => 'toad']); + $context = \context::instance_by_id($catwithid2->contextid); // The hidden question should not appear in the counts. $this->assertEquals([ '' => 'Choose...', 'pqr789' => 'Second category with [pqr789] (1)', - ], utils::get_categories_with_sharable_question_choices(\context_system::instance())); + ], utils::get_categories_with_sharable_question_choices($context)); } public function test_behaviour_choices(): void { @@ -327,6 +330,7 @@ public function test_behaviour_choices(): void { * */ public function test_create_attempt_at_embedded_question(): void { + global $COURSE; $this->setAdminUser(); $this->resetAfterTest(); @@ -337,9 +341,12 @@ public function test_create_attempt_at_embedded_question(): void { // Create course. $course = $generator->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']); - $coursecontext = \context_course::instance($course->id); + // In unit test, the global $COURSE is always set to SITE, so we need to set it to the course we created. + $COURSE = $course; + $qbank = $generator->create_module('qbank', ['course' => $course->id], ['idnumber' => 'qbank1']); + $context = \context_module::instance($qbank->cmid); // Create embed question. - $question = $attemptgenerator->create_embeddable_question('truefalse', null, [], ['contextid' => $coursecontext->id]); + $question = $attemptgenerator->create_embeddable_question('truefalse', null, [], ['contextid' => $context->id]); // Create page page that embeds a question. $page = $generator->create_module('page', [ 'course' => $course->id, @@ -364,10 +371,11 @@ public function test_get_question_bank_url(): void { $course = $this->getDataGenerator()->create_course(); /** @var \core_question_generator $questiongenerator */ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]); + $context = \context_module::instance($qbank->cmid); // Create a question with two versions. - $cat = $questiongenerator->create_question_category( - ['contextid' => \context_course::instance($course->id)->id]); + $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); $firstversion = \question_bank::load_question($saq->id); @@ -377,7 +385,7 @@ public function test_get_question_bank_url(): void { // Prepare the expected result. $expectedurl = new \moodle_url('/question/edit.php', [ - 'courseid' => $course->id, + 'cmid' => $context->instanceid, 'cat' => $secondversion->category . ',' . $secondversion->contextid, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE, 'lastchanged' => $secondversion->id, @@ -389,4 +397,68 @@ public function test_get_question_bank_url(): void { // Check the URL using the second question id. $this->assertEquals($expectedurl, utils::get_question_bank_url($secondversion)); } + + /** + * Test getting shareable question banks. + */ + public function test_get_shareable_question_banks(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']); + // Create a question bank. + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']); + $qbank2 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => 'qbank2']); + $generator = $this->getDataGenerator(); + $attemptgenerator = $generator->get_plugin_generator('filter_embedquestion'); + $attemptgenerator->create_embeddable_question('truefalse', null, [], + ['contextid' => \context_module::instance($qbank->cmid)->id]); + $attemptgenerator->create_embeddable_question('truefalse', null, [], + ['contextid' => \context_module::instance($qbank2->cmid)->id]); + + $banks = utils::get_shareable_question_banks($course->id); + $this->assertArrayHasKey($qbank->cmid, $banks); + $this->assertArrayHasKey($qbank2->cmid, $banks); + + $banks = utils::get_shareable_question_banks($course->id, $course->shortname, null, 'qbank2'); + $this->assertArrayHasKey($qbank2->cmid, $banks); + $this->assertArrayNotHasKey($qbank->cmid, $banks); + } + /** + * Test getting a question bank by idnumber. + */ + public function test_get_qbank_by_idnumber(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(['fullname' => 'Course 1', 'shortname' => 'C1']); + // Create a question bank. + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']); + $qbank2 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => 'abcd1234']); + + $generator = $this->getDataGenerator(); + $attemptgenerator = $generator->get_plugin_generator('filter_embedquestion'); + $attemptgenerator->create_embeddable_question('truefalse', null, [], + ['contextid' => \context_module::instance($qbank->cmid)->id]); + $attemptgenerator->create_embeddable_question('truefalse', null, [], + ['contextid' => \context_module::instance($qbank2->cmid)->id]); + + // Check that we can get the question bank. + $this->assertEquals($qbank->cmid, utils::get_qbank_by_idnumber($course->id)); + $this->assertEquals($qbank2->cmid, utils::get_qbank_by_idnumber($course->id, '', 'abcd1234')); + // We can get the question bank by with idnumber using course short name. + $this->assertEquals($qbank2->cmid, utils::get_qbank_by_idnumber(SITEID, 'C1', 'abcd1234')); + + $qbank3 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']); + $attemptgenerator->create_embeddable_question('truefalse', null, [], + ['contextid' => \context_module::instance($qbank3->cmid)->id]); + // Can't get the correct question bank if there are multiple banks without idnumber. + $this->assertEquals(-1, utils::get_qbank_by_idnumber($course->id)); + // Can't get a question bank doesn't exist in the course. + $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, 'C2')); + // Can't get a question bank with an idnumber that does not exist. + $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, '', 'randomidnumber')); + } } From bbb35c6820c3a8bb5bd8e1456509a0391fd52a1a Mon Sep 17 00:00:00 2001 From: hieuvu Date: Tue, 16 Sep 2025 13:24:44 +0700 Subject: [PATCH 2/6] Add switch question bank temlate and update JS logic to handle embed question. --- .../modal_embedquestion_question_bank.min.js | 3 + ...dal_embedquestion_question_bank.min.js.map | 1 + amd/build/questionid_choice_updater.min.js | 2 +- .../questionid_choice_updater.min.js.map | 2 +- amd/src/modal_embedquestion_question_bank.js | 169 ++++++++++++++++++ amd/src/questionid_choice_updater.js | 90 +++++++++- classes/external.php | 43 +++-- .../get_sharable_categories_choices.php | 95 ++++++++++ classes/output/switch_question_bank.php | 72 ++++++++ db/services.php | 9 +- templates/switch_question_bank.mustache | 131 ++++++++++++++ version.php | 2 +- 12 files changed, 595 insertions(+), 24 deletions(-) create mode 100644 amd/build/modal_embedquestion_question_bank.min.js create mode 100644 amd/build/modal_embedquestion_question_bank.min.js.map create mode 100644 amd/src/modal_embedquestion_question_bank.js create mode 100644 classes/external/get_sharable_categories_choices.php create mode 100644 classes/output/switch_question_bank.php create mode 100644 templates/switch_question_bank.mustache diff --git a/amd/build/modal_embedquestion_question_bank.min.js b/amd/build/modal_embedquestion_question_bank.min.js new file mode 100644 index 0000000..a07a77c --- /dev/null +++ b/amd/build/modal_embedquestion_question_bank.min.js @@ -0,0 +1,3 @@ +define("filter_embedquestion/modal_embedquestion_question_bank",["exports","mod_quiz/add_question_modal","core/fragment","core/str","core/form-autocomplete"],(function(_exports,_add_question_modal,Fragment,_str,_formAutocomplete){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=_exports.ModalEmbedQuestionQuestionBank=void 0,_add_question_modal=_interopRequireDefault(_add_question_modal),Fragment=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete);const SELECTORS={SWITCH_TO_OTHER_BANK:'button[data-action="switch-question-bank"]',BANK_SEARCH:"#searchbanks",NEW_BANKMOD_ID:"data-newmodid",ANCHOR:"a[href]",SORTERS:".sorters",GO_BACK_BUTTON:'button[data-action="go-back"]'};class ModalEmbedQuestionQuestionBank extends _add_question_modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.courseId=modalConfig.courseId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,this.currentEditor=modalConfig.editor,super.configure(modalConfig)}show(){return this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH),super.show(this)}switchToEmbedQuestionModal(bankCmid){this.destroy();const event=new CustomEvent("tiny_embedquestion::displayDialog",{detail:{bankCmid:bankCmid,editor:this.currentEditor}});document.dispatchEvent(event)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("click",SELECTORS.ANCHOR,(e=>{const anchorElement=e.currentTarget;e.preventDefault(),this.switchToEmbedQuestionModal(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID))})),this.getModal().on("click",SELECTORS.GO_BACK_BUTTON,(e=>{e.preventDefault(),this.switchToEmbedQuestionModal(e.currentTarget.value)}))}async handleSwitchBankContentReload(Selector){var _document$querySelect;this.setTitle((0,_str.getString)("selectquestionbank","mod_quiz"));const el=document.createElement("button");el.classList.add("btn","btn-primary"),el.textContent=await(0,_str.getString)("gobacktoquiz","mod_quiz"),el.setAttribute("data-action","go-back"),el.setAttribute("value",this.bankCmId),this.setFooter(el),this.setBody(Fragment.loadFragment("filter_embedquestion","switch_question_bank",this.getContextId(),{courseid:this.courseId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");await this.getBodyPromise(),await _formAutocomplete.default.enhance(Selector,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),null===(_document$querySelect=document.querySelector(".search-banks .form-autocomplete-selection"))||void 0===_document$querySelect||_document$querySelect.classList.add("d-none");const bankSearchEl=document.querySelector(Selector);return bankSearchEl&&bankSearchEl.addEventListener("change",(e=>{const selectedValue=e.target.value;selectedValue>0&&this.switchToEmbedQuestionModal(selectedValue)})),this}}var obj,key,value;_exports.ModalEmbedQuestionQuestionBank=ModalEmbedQuestionQuestionBank,value="filter_embedquestion-question-bank",(key="TYPE")in(obj=ModalEmbedQuestionQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value;var _default={ModalEmbedQuestionQuestionBank:ModalEmbedQuestionQuestionBank,SELECTORS:SELECTORS};return _exports.default=_default,ModalEmbedQuestionQuestionBank.registerModalType(),_exports.default})); + +//# sourceMappingURL=modal_embedquestion_question_bank.min.js.map \ No newline at end of file diff --git a/amd/build/modal_embedquestion_question_bank.min.js.map b/amd/build/modal_embedquestion_question_bank.min.js.map new file mode 100644 index 0000000..449ce65 --- /dev/null +++ b/amd/build/modal_embedquestion_question_bank.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"modal_embedquestion_question_bank.min.js","sources":["../src/modal_embedquestion_question_bank.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module filter_embedquestion/modal_embedquestion_question_bank\n * @copyright 2025 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Modal from 'mod_quiz/add_question_modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\n\nconst SELECTORS = {\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n BANK_SEARCH: '#searchbanks',\n NEW_BANKMOD_ID: 'data-newmodid',\n ANCHOR: 'a[href]',\n SORTERS: '.sorters',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n};\n\n/**\n * Class representing a modal for selecting a question bank to embed questions from.\n */\nexport class ModalEmbedQuestionQuestionBank extends Modal {\n static TYPE = 'filter_embedquestion-question-bank';\n\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n this.courseId = modalConfig.courseId;\n this.bankCmId = modalConfig.bankCmId;\n // Store the original title of the modal, so we can revert back to it once we have switched to another bank.\n this.originalTitle = modalConfig.title;\n this.currentEditor = modalConfig.editor;\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n /**\n * Show the modal and load the content for switching question banks.\n *\n * @method show\n */\n show() {\n this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH);\n return super.show(this);\n }\n\n /**\n * Switch to the embed question modal for a specific question bank.\n * This will destroy the current modal and dispatch an event to switch to the new modal.\n *\n * @param {String} bankCmid - The course module ID of the question bank to switch to.\n * @method switchToEmbedQuestionModal\n */\n switchToEmbedQuestionModal(bankCmid) {\n this.destroy();\n const event = new CustomEvent('tiny_embedquestion::displayDialog', {\n detail: {bankCmid: bankCmid, editor: this.currentEditor},\n });\n document.dispatchEvent(event);\n }\n\n /**\n * Set up all the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = e.currentTarget;\n e.preventDefault();\n this.switchToEmbedQuestionModal(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID));\n });\n\n this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {\n e.preventDefault();\n this.switchToEmbedQuestionModal(e.currentTarget.value);\n });\n }\n\n /**\n * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields.\n *\n * @param {String} Selector for the original select element.\n * @return {Promise} Modal.\n */\n async handleSwitchBankContentReload(Selector) {\n this.setTitle(getString('selectquestionbank', 'mod_quiz'));\n\n // Create a 'Go back' button and set it in the footer.\n const el = document.createElement('button');\n el.classList.add('btn', 'btn-primary');\n el.textContent = await getString('gobacktoquiz', 'mod_quiz');\n el.setAttribute('data-action', 'go-back');\n el.setAttribute('value', this.bankCmId);\n this.setFooter(el);\n\n this.setBody(\n Fragment.loadFragment(\n 'filter_embedquestion',\n 'switch_question_bank',\n this.getContextId(),\n {\n 'courseid': this.courseId,\n })\n );\n const placeholder = await getString('searchbyname', 'mod_quiz');\n await this.getBodyPromise();\n await AutoComplete.enhance(\n Selector,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true\n );\n\n // Hide the selection element as we don't need it.\n document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none');\n // Add a change listener to get the selected value.\n const bankSearchEl = document.querySelector(Selector);\n if (bankSearchEl) {\n bankSearchEl.addEventListener('change', (e) => {\n // This will be the chosen qbankCmid.\n const selectedValue = e.target.value;\n if (selectedValue > 0) {\n this.switchToEmbedQuestionModal(selectedValue);\n }\n });\n }\n return this;\n }\n}\n\nexport default {\n ModalEmbedQuestionQuestionBank,\n SELECTORS\n};\nModalEmbedQuestionQuestionBank.registerModalType();"],"names":["SELECTORS","SWITCH_TO_OTHER_BANK","BANK_SEARCH","NEW_BANKMOD_ID","ANCHOR","SORTERS","GO_BACK_BUTTON","ModalEmbedQuestionQuestionBank","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","courseId","bankCmId","originalTitle","title","currentEditor","editor","handleSwitchBankContentReload","super","this","switchToEmbedQuestionModal","bankCmid","destroy","event","CustomEvent","detail","document","dispatchEvent","registerEventListeners","getModal","on","e","anchorElement","currentTarget","preventDefault","getAttribute","value","Selector","setTitle","el","createElement","classList","add","textContent","setAttribute","setFooter","setBody","Fragment","loadFragment","getContextId","placeholder","getBodyPromise","AutoComplete","enhance","querySelector","bankSearchEl","addEventListener","selectedValue","target","registerModalType"],"mappings":"q+CA2BMA,UAAY,CACdC,qBAAsB,6CACtBC,YAAa,eACbC,eAAgB,gBAChBC,OAAQ,UACRC,QAAS,WACTC,eAAgB,uCAMPC,uCAAuCC,4BAGhDC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,gBAC3BC,SAAWR,YAAYQ,cACvBC,SAAWT,YAAYS,cAEvBC,cAAgBV,YAAYW,WAC5BC,cAAgBZ,YAAYa,aAE3Bd,UAAUC,aAQpBE,mBACSY,8BAA8BxB,UAAUE,aACtCuB,MAAMb,KAAKc,MAUtBC,2BAA2BC,eAClBC,gBACCC,MAAQ,IAAIC,YAAY,oCAAqC,CAC/DC,OAAQ,CAACJ,SAAUA,SAAUL,OAAQG,KAAKJ,iBAE9CW,SAASC,cAAcJ,OAQ3BK,+BAEUA,uBAAuBT,WAExBU,WAAWC,GAAG,QAASrC,UAAUI,QAASkC,UACrCC,cAAgBD,EAAEE,cACxBF,EAAEG,sBACGd,2BAA2BY,cAAcG,aAAa1C,UAAUG,yBAGpEiC,WAAWC,GAAG,QAASrC,UAAUM,gBAAiBgC,IACnDA,EAAEG,sBACGd,2BAA2BW,EAAEE,cAAcG,8CAUpBC,yCAC3BC,UAAS,kBAAU,qBAAsB,mBAGxCC,GAAKb,SAASc,cAAc,UAClCD,GAAGE,UAAUC,IAAI,MAAO,eACxBH,GAAGI,kBAAoB,kBAAU,eAAgB,YACjDJ,GAAGK,aAAa,cAAe,WAC/BL,GAAGK,aAAa,QAASzB,KAAKP,eACzBiC,UAAUN,SAEVO,QACDC,SAASC,aACL,uBACA,uBACA7B,KAAK8B,eACL,UACgB9B,KAAKR,kBAGvBuC,kBAAoB,kBAAU,eAAgB,kBAC9C/B,KAAKgC,uBACLC,0BAAaC,QACfhB,UACA,EACA,0CACAa,aACA,GACA,EACA,IACA,iCAIJxB,SAAS4B,cAAc,sGAA+Cb,UAAUC,IAAI,gBAE9Ea,aAAe7B,SAAS4B,cAAcjB,iBACxCkB,cACAA,aAAaC,iBAAiB,UAAWzB,UAE/B0B,cAAgB1B,EAAE2B,OAAOtB,MAC3BqB,cAAgB,QACXrC,2BAA2BqC,kBAIrCtC,qGAxHG,wDADLnB,mJA6HE,CACXA,+BAAAA,+BACAP,UAAAA,4CAEJO,+BAA+B2D"} \ No newline at end of file diff --git a/amd/build/questionid_choice_updater.min.js b/amd/build/questionid_choice_updater.min.js index 74a8625..ad66386 100644 --- a/amd/build/questionid_choice_updater.min.js +++ b/amd/build/questionid_choice_updater.min.js @@ -6,6 +6,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("filter_embedquestion/questionid_choice_updater",["jquery","core/ajax"],(function($,Ajax){var t={init:function(){$("select#id_categoryidnumber").on("change",t.categoryChanged),t.lastCategory=null},lastCategory:null,categoryChanged:function(){$("select#id_categoryidnumber").val()!==t.lastCategory&&(M.util.js_pending("filter_embedquestion-get_questions"),t.lastCategory=$("select#id_categoryidnumber").val(),""===t.lastCategory?t.updateChoices([]):Ajax.call([{methodname:"filter_embedquestion_get_sharable_question_choices",args:{courseid:$("input[name=courseid]").val(),categoryidnumber:t.lastCategory}}])[0].done(t.updateChoices))},updateChoices:function(response){var select=$("select#id_questionidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_questions")}};return t})); +define("filter_embedquestion/questionid_choice_updater",["jquery","core/ajax","core/str","core/notification","core_user/repository"],(function($,Ajax,Str,Notification,UserRepository){var t={init:function(defaultQbank){$("select#id_qbankcmid").on("change",t.qbankChanged),$("select#id_categoryidnumber").on("change",t.categoryChanged),t.lastQbank=$("select#id_qbankcmid").val(),t.lastCategory=$("select#id_categoryidnumber").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception),defaultQbank&&$("select#id_qbankcmid").val(defaultQbank).trigger("change")},lastCategory:null,lastQbank:null,categoryChanged:function(){M.util.js_pending("filter_embedquestion-get_questions"),t.lastCategory=$("select#id_categoryidnumber").val(),""===t.lastCategory?t.updateChoices([]):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_question_choices",args:{cmid:t.lastQbank,categoryidnumber:t.lastCategory}}])[0].done(t.updateChoices),$("select#id_questionidnumber").attr("disabled",!1))},qbankChanged:function(){if($("select#id_qbankcmid").val()!==t.lastQbank){M.util.js_pending("filter_embedquestion-get_categories"),t.lastQbank=$("select#id_qbankcmid").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception);var prefKey="filter_embedquestion_userdefaultqbank",courseId=document.querySelector('input[name="courseid"]').value,courseShortname=document.querySelector('input[name="courseshortname"]').value;""!==courseShortname&&null!==courseShortname||UserRepository.getUserPreference(prefKey).then((current=>{let prefs=current?JSON.parse(current):{};return prefs[courseId]=t.lastQbank,UserRepository.setUserPreference(prefKey,JSON.stringify(prefs))})).catch(Notification.exception),""===$("select#id_qbankcmid").val()?(t.updateCategories([]),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([])):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_categories_choices",args:{cmid:t.lastQbank}}])[0].done(t.updateCategories),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([]))}},updateCategories:function(response){var select=$("select#id_categoryidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_categories")},updateChoices:function(response){var select=$("select#id_questionidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_questions")}};return t})); //# sourceMappingURL=questionid_choice_updater.min.js.map \ No newline at end of file diff --git a/amd/build/questionid_choice_updater.min.js.map b/amd/build/questionid_choice_updater.min.js.map index 596a63f..bae3601 100644 --- a/amd/build/questionid_choice_updater.min.js.map +++ b/amd/build/questionid_choice_updater.min.js.map @@ -1 +1 @@ -{"version":3,"file":"questionid_choice_updater.min.js","sources":["../src/questionid_choice_updater.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * The module provides autocomplete for the question idnumber form field.\n *\n * @module filter_embedquestion/questionid_choice_updater\n * @package filter_embedquestion\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax'], function($, Ajax) {\n var t = {\n /**\n * Initialise the handling.\n */\n init: function() {\n $('select#id_categoryidnumber').on('change', t.categoryChanged);\n t.lastCategory = null;\n },\n\n /**\n * Used to track when the category really changes.\n */\n lastCategory: null,\n\n /**\n * Source of data for Ajax element.\n */\n categoryChanged: function() {\n if ($('select#id_categoryidnumber').val() === t.lastCategory) {\n return;\n }\n\n M.util.js_pending('filter_embedquestion-get_questions');\n t.lastCategory = $('select#id_categoryidnumber').val();\n\n if (t.lastCategory === '') {\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_question_choices',\n args: {courseid: $('input[name=courseid]').val(), categoryidnumber: t.lastCategory}\n }])[0].done(t.updateChoices);\n }\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateChoices: function(response) {\n var select = $('select#id_questionidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_questions');\n }\n };\n return t;\n});\n"],"names":["define","$","Ajax","t","init","on","categoryChanged","lastCategory","val","M","util","js_pending","updateChoices","call","methodname","args","courseid","categoryidnumber","done","response","select","empty","each","index","option","append","value","label","js_complete"],"mappings":";;;;;;;;AAuBAA,wDAAO,CAAC,SAAU,cAAc,SAASC,EAAGC,UACpCC,EAAI,CAIJC,KAAM,WACFH,EAAE,8BAA8BI,GAAG,SAAUF,EAAEG,iBAC/CH,EAAEI,aAAe,MAMrBA,aAAc,KAKdD,gBAAiB,WACTL,EAAE,8BAA8BO,QAAUL,EAAEI,eAIhDE,EAAEC,KAAKC,WAAW,sCAClBR,EAAEI,aAAeN,EAAE,8BAA8BO,MAE1B,KAAnBL,EAAEI,aACFJ,EAAES,cAAc,IAEhBV,KAAKW,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,SAAUf,EAAE,wBAAwBO,MAAOS,iBAAkBd,EAAEI,iBACtE,GAAGW,KAAKf,EAAES,iBAStBA,cAAe,SAASO,cAChBC,OAASnB,EAAE,8BAEfmB,OAAOC,QACPpB,EAAEkB,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOE,MAAQ,KAAOF,OAAOG,MAAQ,gBAE3ElB,EAAEC,KAAKkB,YAAY,+CAGpBzB"} \ No newline at end of file +{"version":3,"file":"questionid_choice_updater.min.js","sources":["../src/questionid_choice_updater.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * The module provides autocomplete for the question idnumber form field.\n *\n * @module filter_embedquestion/questionid_choice_updater\n * @package filter_embedquestion\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repository'],\n function($, Ajax, Str, Notification, UserRepository) {\n var t = {\n /**\n * Initialise the handling.\n *\n * @param {string} defaultQbank - The default question bank to select, if any.\n */\n init: function(defaultQbank) {\n $('select#id_qbankcmid').on('change', t.qbankChanged);\n $('select#id_categoryidnumber').on('change', t.categoryChanged);\n\n t.lastQbank = $('select#id_qbankcmid').val();\n t.lastCategory = $('select#id_categoryidnumber').val();\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n if (defaultQbank) {\n // If a default question bank is set, we need to trigger the change event to load the categories.\n $('select#id_qbankcmid').val(defaultQbank).trigger('change');\n }\n },\n\n /**\n * Used to track when the category really changes.\n */\n lastCategory: null,\n /**\n * Used to track when the question bank really changes.\n */\n lastQbank: null,\n\n /**\n * Source of data for Ajax element.\n */\n categoryChanged: function() {\n M.util.js_pending('filter_embedquestion-get_questions');\n t.lastCategory = $('select#id_categoryidnumber').val();\n if (t.lastCategory === '') {\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_question_choices',\n args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory},\n }])[0].done(t.updateChoices);\n $('select#id_questionidnumber').attr('disabled', false);\n }\n },\n\n /**\n * Source of data for Ajax element.\n */\n qbankChanged: function() {\n if ($('select#id_qbankcmid').val() === t.lastQbank) {\n return;\n }\n M.util.js_pending('filter_embedquestion-get_categories');\n t.lastQbank = $('select#id_qbankcmid').val();\n // Update the heading immediately when selection changes.\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n var prefKey = 'filter_embedquestion_userdefaultqbank';\n var courseId = document.querySelector('input[name=\"courseid\"]').value;\n var courseShortname = document.querySelector('input[name=\"courseshortname\"]').value;\n if (courseShortname === '' || courseShortname === null) {\n UserRepository.getUserPreference(prefKey).then(current => {\n let prefs = current ? JSON.parse(current) : {};\n prefs[courseId] = t.lastQbank;\n return UserRepository.setUserPreference(prefKey, JSON.stringify(prefs));\n }).catch(Notification.exception);\n }\n if ($('select#id_qbankcmid').val() === '') {\n t.updateCategories([]);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_categories_choices',\n args: {cmid: t.lastQbank}\n }])[0].done(t.updateCategories);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n }\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateCategories: function(response) {\n var select = $('select#id_categoryidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_categories');\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateChoices: function(response) {\n var select = $('select#id_questionidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_questions');\n }\n };\n return t;\n});\n"],"names":["define","$","Ajax","Str","Notification","UserRepository","t","init","defaultQbank","on","qbankChanged","categoryChanged","lastQbank","val","lastCategory","selectedText","text","get_string","then","string","catch","exception","trigger","M","util","js_pending","updateChoices","call","methodname","args","cmid","categoryidnumber","done","attr","prefKey","courseId","document","querySelector","value","courseShortname","getUserPreference","current","prefs","JSON","parse","setUserPreference","stringify","updateCategories","response","select","empty","each","index","option","append","label","js_complete"],"mappings":";;;;;;;;AAwBAA,wDAAO,CAAC,SAAU,YAAa,WAAY,oBAAqB,yBACxD,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,oBACrCC,EAAI,CAMJC,KAAM,SAASC,cACXP,EAAE,uBAAuBQ,GAAG,SAAUH,EAAEI,cACxCT,EAAE,8BAA8BQ,GAAG,SAAUH,EAAEK,iBAE/CL,EAAEM,UAAYX,EAAE,uBAAuBY,MACvCP,EAAEQ,aAAeb,EAAE,8BAA8BY,UAC7CE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,WACtBb,cAEAP,EAAE,uBAAuBY,IAAIL,cAAcc,QAAQ,WAO3DR,aAAc,KAIdF,UAAW,KAKXD,gBAAiB,WACbY,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEQ,aAAeb,EAAE,8BAA8BY,MAC1B,KAAnBP,EAAEQ,aACFR,EAAEoB,cAAc,KAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,KAAMxB,EAAEM,UAAWmB,iBAAkBzB,EAAEQ,iBAC9C,GAAGkB,KAAK1B,EAAEoB,eACdzB,EAAE,8BAA8BgC,KAAK,YAAY,KAOzDvB,aAAc,cACNT,EAAE,uBAAuBY,QAAUP,EAAEM,WAGzCW,EAAEC,KAAKC,WAAW,uCAClBnB,EAAEM,UAAYX,EAAE,uBAAuBY,UAEnCE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,eACtBa,QAAU,wCACVC,SAAWC,SAASC,cAAc,0BAA0BC,MAC5DC,gBAAkBH,SAASC,cAAc,iCAAiCC,MACtD,KAApBC,iBAA8C,OAApBA,iBAC1BlC,eAAemC,kBAAkBN,SAAShB,MAAKuB,cACvCC,MAAQD,QAAUE,KAAKC,MAAMH,SAAW,UAC5CC,MAAMP,UAAY7B,EAAEM,UACbP,eAAewC,kBAAkBX,QAASS,KAAKG,UAAUJ,WACjEtB,MAAMhB,aAAaiB,WAEa,KAAnCpB,EAAE,uBAAuBY,OACzBP,EAAEyC,iBAAiB,IACnBxB,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,MAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,uDACZC,KAAM,CAACC,KAAMxB,EAAEM,cACf,GAAGoB,KAAK1B,EAAEyC,kBACdxB,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,OASxBqB,iBAAkB,SAASC,cACnBC,OAAShD,EAAE,8BAEfgD,OAAOC,QACPjD,EAAE+C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOf,MAAQ,KAAOe,OAAOE,MAAQ,gBAE3EhC,EAAEC,KAAKgC,YAAY,wCAQvB9B,cAAe,SAASsB,cAChBC,OAAShD,EAAE,8BAEfgD,OAAOC,QACPjD,EAAE+C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOf,MAAQ,KAAOe,OAAOE,MAAQ,gBAE3EhC,EAAEC,KAAKgC,YAAY,+CAGpBlD"} \ No newline at end of file diff --git a/amd/src/modal_embedquestion_question_bank.js b/amd/src/modal_embedquestion_question_bank.js new file mode 100644 index 0000000..35ed7fe --- /dev/null +++ b/amd/src/modal_embedquestion_question_bank.js @@ -0,0 +1,169 @@ +// 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 . + +/** + * Contain the logic for the question bank modal. + * + * @module filter_embedquestion/modal_embedquestion_question_bank + * @copyright 2025 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import Modal from 'mod_quiz/add_question_modal'; +import * as Fragment from 'core/fragment'; +import {getString} from 'core/str'; +import AutoComplete from 'core/form-autocomplete'; + +const SELECTORS = { + SWITCH_TO_OTHER_BANK: 'button[data-action="switch-question-bank"]', + BANK_SEARCH: '#searchbanks', + NEW_BANKMOD_ID: 'data-newmodid', + ANCHOR: 'a[href]', + SORTERS: '.sorters', + GO_BACK_BUTTON: 'button[data-action="go-back"]', +}; + +/** + * Class representing a modal for selecting a question bank to embed questions from. + */ +export class ModalEmbedQuestionQuestionBank extends Modal { + static TYPE = 'filter_embedquestion-question-bank'; + + configure(modalConfig) { + // Add question modals are always large. + modalConfig.large = true; + + // Always show on creation. + modalConfig.show = true; + modalConfig.removeOnClose = true; + + // Apply question modal configuration. + this.setContextId(modalConfig.contextId); + this.setAddOnPageId(modalConfig.addOnPage); + this.courseId = modalConfig.courseId; + this.bankCmId = modalConfig.bankCmId; + // Store the original title of the modal, so we can revert back to it once we have switched to another bank. + this.originalTitle = modalConfig.title; + this.currentEditor = modalConfig.editor; + // Apply standard configuration. + super.configure(modalConfig); + } + + /** + * Show the modal and load the content for switching question banks. + * + * @method show + */ + show() { + this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH); + return super.show(this); + } + + /** + * Switch to the embed question modal for a specific question bank. + * This will destroy the current modal and dispatch an event to switch to the new modal. + * + * @param {String} bankCmid - The course module ID of the question bank to switch to. + * @method switchToEmbedQuestionModal + */ + switchToEmbedQuestionModal(bankCmid) { + this.destroy(); + const event = new CustomEvent('tiny_embedquestion::displayDialog', { + detail: {bankCmid: bankCmid, editor: this.currentEditor}, + }); + document.dispatchEvent(event); + } + + /** + * Set up all the event handling for the modal. + * + * @method registerEventListeners + */ + registerEventListeners() { + // Apply parent event listeners. + super.registerEventListeners(this); + + this.getModal().on('click', SELECTORS.ANCHOR, (e) => { + const anchorElement = e.currentTarget; + e.preventDefault(); + this.switchToEmbedQuestionModal(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID)); + }); + + this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => { + e.preventDefault(); + this.switchToEmbedQuestionModal(e.currentTarget.value); + }); + } + + /** + * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields. + * + * @param {String} Selector for the original select element. + * @return {Promise} Modal. + */ + async handleSwitchBankContentReload(Selector) { + this.setTitle(getString('selectquestionbank', 'mod_quiz')); + + // Create a 'Go back' button and set it in the footer. + const el = document.createElement('button'); + el.classList.add('btn', 'btn-primary'); + el.textContent = await getString('gobacktoquiz', 'mod_quiz'); + el.setAttribute('data-action', 'go-back'); + el.setAttribute('value', this.bankCmId); + this.setFooter(el); + + this.setBody( + Fragment.loadFragment( + 'filter_embedquestion', + 'switch_question_bank', + this.getContextId(), + { + 'courseid': this.courseId, + }) + ); + const placeholder = await getString('searchbyname', 'mod_quiz'); + await this.getBodyPromise(); + await AutoComplete.enhance( + Selector, + false, + 'core_question/question_banks_datasource', + placeholder, + false, + true, + '', + true + ); + + // Hide the selection element as we don't need it. + document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none'); + // Add a change listener to get the selected value. + const bankSearchEl = document.querySelector(Selector); + if (bankSearchEl) { + bankSearchEl.addEventListener('change', (e) => { + // This will be the chosen qbankCmid. + const selectedValue = e.target.value; + if (selectedValue > 0) { + this.switchToEmbedQuestionModal(selectedValue); + } + }); + } + return this; + } +} + +export default { + ModalEmbedQuestionQuestionBank, + SELECTORS +}; +ModalEmbedQuestionQuestionBank.registerModalType(); \ No newline at end of file diff --git a/amd/src/questionid_choice_updater.js b/amd/src/questionid_choice_updater.js index 8fd38e6..b62b530 100644 --- a/amd/src/questionid_choice_updater.js +++ b/amd/src/questionid_choice_updater.js @@ -21,40 +21,112 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define(['jquery', 'core/ajax'], function($, Ajax) { + +define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repository'], + function($, Ajax, Str, Notification, UserRepository) { var t = { /** * Initialise the handling. + * + * @param {string} defaultQbank - The default question bank to select, if any. */ - init: function() { + init: function(defaultQbank) { + $('select#id_qbankcmid').on('change', t.qbankChanged); $('select#id_categoryidnumber').on('change', t.categoryChanged); - t.lastCategory = null; + + t.lastQbank = $('select#id_qbankcmid').val(); + t.lastCategory = $('select#id_categoryidnumber').val(); + var selectedText = $('#id_qbankcmid option:selected').text(); + Str.get_string('currentbank', 'mod_quiz', selectedText) + .then(function(string) { + $('#id_questionheadercontainer h5').text(string); + return; + }).catch(Notification.exception); + if (defaultQbank) { + // If a default question bank is set, we need to trigger the change event to load the categories. + $('select#id_qbankcmid').val(defaultQbank).trigger('change'); + } }, /** * Used to track when the category really changes. */ lastCategory: null, + /** + * Used to track when the question bank really changes. + */ + lastQbank: null, /** * Source of data for Ajax element. */ categoryChanged: function() { - if ($('select#id_categoryidnumber').val() === t.lastCategory) { - return; - } - M.util.js_pending('filter_embedquestion-get_questions'); t.lastCategory = $('select#id_categoryidnumber').val(); - if (t.lastCategory === '') { t.updateChoices([]); } else { Ajax.call([{ methodname: 'filter_embedquestion_get_sharable_question_choices', - args: {courseid: $('input[name=courseid]').val(), categoryidnumber: t.lastCategory} + args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory}, }])[0].done(t.updateChoices); + $('select#id_questionidnumber').attr('disabled', false); + } + }, + + /** + * Source of data for Ajax element. + */ + qbankChanged: function() { + if ($('select#id_qbankcmid').val() === t.lastQbank) { + return; } + M.util.js_pending('filter_embedquestion-get_categories'); + t.lastQbank = $('select#id_qbankcmid').val(); + // Update the heading immediately when selection changes. + var selectedText = $('#id_qbankcmid option:selected').text(); + Str.get_string('currentbank', 'mod_quiz', selectedText) + .then(function(string) { + $('#id_questionheadercontainer h5').text(string); + return; + }).catch(Notification.exception); + var prefKey = 'filter_embedquestion_userdefaultqbank'; + var courseId = document.querySelector('input[name="courseid"]').value; + var courseShortname = document.querySelector('input[name="courseshortname"]').value; + if (courseShortname === '' || courseShortname === null) { + UserRepository.getUserPreference(prefKey).then(current => { + let prefs = current ? JSON.parse(current) : {}; + prefs[courseId] = t.lastQbank; + return UserRepository.setUserPreference(prefKey, JSON.stringify(prefs)); + }).catch(Notification.exception); + } + if ($('select#id_qbankcmid').val() === '') { + t.updateCategories([]); + M.util.js_pending('filter_embedquestion-get_questions'); + t.updateChoices([]); + } else { + Ajax.call([{ + methodname: 'filter_embedquestion_get_sharable_categories_choices', + args: {cmid: t.lastQbank} + }])[0].done(t.updateCategories); + M.util.js_pending('filter_embedquestion-get_questions'); + t.updateChoices([]); + } + }, + + /** + * Update the contents of the Question select with the results of the AJAX call. + * + * @param {Array} response - array of options, each has fields value and label. + */ + updateCategories: function(response) { + var select = $('select#id_categoryidnumber'); + + select.empty(); + $(response).each(function(index, option) { + select.append(''); + }); + M.util.js_complete('filter_embedquestion-get_categories'); }, /** diff --git a/classes/external.php b/classes/external.php index 5e8233a..e081e61 100644 --- a/classes/external.php +++ b/classes/external.php @@ -16,6 +16,8 @@ namespace filter_embedquestion; +use core\exception\moodle_exception; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -36,7 +38,7 @@ class external extends \external_api { */ public static function get_sharable_question_choices_parameters(): \external_function_parameters { return new \external_function_parameters([ - 'courseid' => new \external_value(PARAM_INT, 'Course id.'), + 'cmid' => new \external_value(PARAM_INT, 'Course module ID'), 'categoryidnumber' => new \external_value(PARAM_RAW, 'Idnumber of the question category.'), ]); } @@ -66,18 +68,18 @@ public static function get_sharable_question_choices_is_allowed_from_ajax(): boo /** * Get the list of sharable questions in a category. * - * @param int $courseid the course whose question bank we are sharing from. + * @param int $cmid the course module id. * @param string $categoryidnumber the idnumber of the question category. * * @return array of arrays with two elements, keys value and label. */ - public static function get_sharable_question_choices(int $courseid, string $categoryidnumber): array { + public static function get_sharable_question_choices(int $cmid, string $categoryidnumber): array { global $USER; self::validate_parameters(self::get_sharable_question_choices_parameters(), - ['courseid' => $courseid, 'categoryidnumber' => $categoryidnumber]); + ['cmid' => $cmid, 'categoryidnumber' => $categoryidnumber]); - $context = \context_course::instance($courseid); + $context = \context_module::instance($cmid); self::validate_context($context); if (has_capability('moodle/question:useall', $context)) { @@ -141,6 +143,10 @@ public static function get_embed_code_parameters(): \external_function_parameter 'Whether to show the response history (1/0/"") for show, hide or default.'), 'forcedlanguage' => new \external_value(PARAM_LANG, 'Whether to force the UI language of the question. Lang code or empty string.'), + 'courseshortname' => new \external_value(PARAM_RAW, + 'Course short name.', VALUE_OPTIONAL), + 'questionbankidnumber' => new \external_value(PARAM_RAW, + 'Qbank idnumber.', VALUE_OPTIONAL), ]); } @@ -166,7 +172,7 @@ public static function get_embed_code_is_allowed_from_ajax(): bool { * Given the course id, category and question idnumbers, and any display options, * return the {Q{...}Q} code needed to embed this question. * - * @param int $courseid the id of the course we are embedding questions from. + * @param int $courseid the course id. * @param string $categoryidnumber the idnumber of the question category. * @param string $questionidnumber the idnumber of the question to be embedded, or '*' to mean a question picked at random. * @param string $iframedescription the iframe description. @@ -181,13 +187,15 @@ public static function get_embed_code_is_allowed_from_ajax(): bool { * @param string $rightanswer 0, 1 or ''. * @param string $history 0, 1 or ''. * @param string $forcedlanguage moodle lang pack (e.g. 'fr') or ''. + * @param string|null $courseshortname the course shortname, optional. + * @param string|null $questionbankidnumber the question bank idnumber, optional. * * @return string the embed code. */ public static function get_embed_code(int $courseid, string $categoryidnumber, string $questionidnumber, string $iframedescription, string $behaviour, string $maxmark, string $variant, string $correctness, string $marks, string $markdp, string $feedback, string $generalfeedback, string $rightanswer, string $history, - string $forcedlanguage): string { + string $forcedlanguage, ?string $courseshortname = null, ?string $questionbankidnumber = null): string { global $CFG; self::validate_parameters( @@ -208,25 +216,38 @@ public static function get_embed_code(int $courseid, string $categoryidnumber, s 'rightanswer' => $rightanswer, 'history' => $history, 'forcedlanguage' => $forcedlanguage, + 'courseshortname' => $courseshortname, + 'questionbankidnumber' => $questionbankidnumber, ] ); - $context = \context_course::instance($courseid); + $cmid = utils::get_qbank_by_idnumber($courseid, $courseshortname, $questionbankidnumber); + if ($cmid === -1) { + throw new moodle_exception('invalidquestionbank', 'filter_embedquestion'); + } + $context = \context_module::instance($cmid); self::validate_context($context); - // Check permissions. require_once($CFG->libdir . '/questionlib.php'); $category = utils::get_category_by_idnumber($context, $categoryidnumber); if ($questionidnumber === '*') { - $context = \context_course::instance($courseid); require_capability('moodle/question:useall', $context); } else { $questiondata = utils::get_question_by_idnumber($category->id, $questionidnumber); $question = \question_bank::load_question($questiondata->id); question_require_capability_on($question, 'use'); } + // When we get the question bank created by system in a different course, usually they don't have idnumber + // So we need to add '*' to questionbankidnumber to make sure the question bank can be found. + if (empty($questionbankidnumber) && $courseshortname) { + $course = get_course($courseid); + if ($courseshortname !== $course->shortname) { + $questionbankidnumber = '*'; + } + } $fromform = new \stdClass(); - $fromform->courseid = $courseid; + $fromform->questionbankidnumber = $questionbankidnumber; + $fromform->courseshortname = $courseshortname; $fromform->categoryidnumber = $categoryidnumber; $fromform->questionidnumber = $questionidnumber; $fromform->iframedescription = $iframedescription; diff --git a/classes/external/get_sharable_categories_choices.php b/classes/external/get_sharable_categories_choices.php new file mode 100644 index 0000000..339f145 --- /dev/null +++ b/classes/external/get_sharable_categories_choices.php @@ -0,0 +1,95 @@ +. + +namespace filter_embedquestion\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_description; +use filter_embedquestion\utils; + +/** + * External API to get the list of sharable question categories. + * + * @package filter_embedquestion + * @copyright 2025 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_sharable_categories_choices extends external_api { + /** + * Returns parameter types for get_sharable_categories_choices function. + * + * @return external_function_parameters Parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'cmid' => new external_value(PARAM_INT, 'Course module ID'), + ]); + } + + /** + * Returns result type for get_sharable_categories_choices function. + * + * @return external_description Result type + */ + public static function execute_returns(): external_description { + return new external_multiple_structure( + new external_single_structure([ + 'value' => new external_value(PARAM_RAW, 'Choice value to return from the form.'), + 'label' => new external_value(PARAM_RAW, 'Choice name, to display to users.'), + ])); + } + + /** + * Get the list of sharable categories. + * + * @param int $cmid the course module ID of the question bank. + * + * @return array of arrays with two elements, keys value and label. + */ + public static function execute(int $cmid): array { + global $USER; + + self::validate_parameters(self::execute_parameters(), + ['cmid' => $cmid]); + + $context = \context_module::instance($cmid); + self::validate_context($context); + + if (has_capability('moodle/question:useall', $context)) { + $userlimit = null; + + } else if (has_capability('moodle/question:usemine', $context)) { + $userlimit = $USER->id; + } else { + throw new \coding_exception('This user is not allowed to embed questions.'); + } + + $categories = utils::get_categories_with_sharable_question_choices($context, $userlimit); + if (!$categories) { + throw new \coding_exception('Unknown question category.'); + } + + $out = []; + foreach ($categories as $value => $label) { + $out[] = ['value' => $value, 'label' => $label]; + } + return $out; + } +} diff --git a/classes/output/switch_question_bank.php b/classes/output/switch_question_bank.php new file mode 100644 index 0000000..827712c --- /dev/null +++ b/classes/output/switch_question_bank.php @@ -0,0 +1,72 @@ +. + +/** + * Switch question bank output class for the embed question filter. + * + * @package filter_embedquestion + * @copyright 2025 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace filter_embedquestion\output; + +use core_question\local\bank\question_bank_helper; +use renderer_base; + +/** + * Get the switch question bank rendered content. Displays lists of shared banks the viewing user has access to. + */ +class switch_question_bank implements \renderable, \templatable { + + /** + * Instantiate the output class. + * + * @param int $courseid of the current course. + * @param int $userid of the user viewing the page. + */ + public function __construct( + /** @var int id of the current course */ + private readonly int $courseid, + /** @var int id of the user viewing the page */ + private readonly int $userid + ) { + } + + /** + * Create a list of question banks the user has access to for the template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output) { + $capabilities = ['moodle/question:useall', 'moodle/question:usemine']; + $contextcourse = \context_course::instance($this->courseid); + $coursesharedbanks = question_bank_helper::get_activity_instances_with_shareable_questions( + incourseids: [$this->courseid], + havingcap: $capabilities, + ); + $recentlyviewedbanks = question_bank_helper::get_recently_used_open_banks($this->userid, havingcap: $capabilities); + + return [ + 'hascoursesharedbanks' => !empty($coursesharedbanks), + 'coursesharedbanks' => $coursesharedbanks, + 'hasrecentlyviewedbanks' => !empty($recentlyviewedbanks), + 'recentlyviewedbanks' => $recentlyviewedbanks, + 'contextid' => $contextcourse->id, + ]; + } +} diff --git a/db/services.php b/db/services.php index c15d3c4..e269157 100644 --- a/db/services.php +++ b/db/services.php @@ -34,11 +34,18 @@ 'ajax' => true, ], + 'filter_embedquestion_get_sharable_categories_choices' => [ + 'classname' => 'filter_embedquestion\external\get_sharable_categories_choices', + 'description' => 'Use by form autocomplete for selecting a sharable qbank.', + 'type' => 'read', + 'ajax' => true, + ], + 'filter_embedquestion_get_embed_code' => [ 'classname' => 'filter_embedquestion\external', 'methodname' => 'get_embed_code', 'classpath' => '', - 'description' => 'Use by atto-editer embedquestion button.', + 'description' => 'Use by tiny/atto embedquestion button.', 'type' => 'read', 'ajax' => true, ], diff --git a/templates/switch_question_bank.mustache b/templates/switch_question_bank.mustache new file mode 100644 index 0000000..db00e60 --- /dev/null +++ b/templates/switch_question_bank.mustache @@ -0,0 +1,131 @@ +{{! + 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 filter_embedquestion/switch_question_bank + + Example context (json): +{ + "contextid": "2", + "hascoursesharedbanks": true, + "coursesharedbanks": [ + { + "name": "Question bank 1", + "modid": "2", + "contextid": 2, + "coursenamebankname": "c1 - Question bank 1", + "cminfo": {}, + "questioncategories": [] + }, + { + "name": "Question bank 2", + "modid": "3", + "contextid": 3, + "coursenamebankname": "c1 - Question bank 2", + "cminfo": {}, + "questioncategories": [] + } + ], + "hasrecentlyviewedbanks": true, + "recentlyviewedbanks": [ + { + "name": "Question bank 3", + "modid": "4", + "contextid": 4, + "coursenamebankname": "c2 - Question bank 4", + "cminfo": {}, + "questioncategories": [] + }, + { + "name": "Question bank 4", + "modid": "6", + "contextid": 6, + "coursenamebankname": "c3 - Question bank 5", + "cminfo": {}, + "questioncategories": [] + } + ], + "hassharedbanks": true, + "sharedbanks": [ + { + "name": "Question bank 1", + "modid": "2", + "contextid": 2, + "coursenamebankname": "c1 - Question bank 1", + "cminfo": {}, + "questioncategories": [] + }, + { + "name": "Question bank 2", + "modid": "3", + "contextid": 3, + "coursenamebankname": "c1 - Question bank 2", + "cminfo": {}, + "questioncategories": [] + }, + { + "name": "Question bank 3", + "modid": "4", + "contextid": 4, + "coursenamebankname": "c2 - Question bank 4", + "cminfo": {}, + "questioncategories": [] + }, + { + "name": "Question bank 4", + "modid": "6", + "contextid": 6, + "coursenamebankname": "c3 - Question bank 5", + "cminfo": {}, + "questioncategories": [] + } + ] +} +}} +{{#hascoursesharedbanks}} +
+
{{#str}}banksincourse, core_question{{/str}}
+ {{#coursesharedbanks}} + + {{/coursesharedbanks}} +
+
+{{/hascoursesharedbanks}} + +{{#hasrecentlyviewedbanks}} +
+
{{#str}}recentlyviewedquestionbanks, core_question{{/str}}
+ {{#recentlyviewedbanks}} + + {{/recentlyviewedbanks}} +
+
+{{/hasrecentlyviewedbanks}} + + +
+
{{#str}}otherquestionbank, core_question{{/str}}
+ +
\ No newline at end of file diff --git a/version.php b/version.php index 0e4e65e..2cb36a0 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025050100; +$plugin->version = 2025050102; $plugin->requires = 2024042200; // Requires Moodle 4.4. $plugin->component = 'filter_embedquestion'; $plugin->maturity = MATURITY_STABLE; From 87febf0aca47e451266b614f8a5b347e0005a9fd Mon Sep 17 00:00:00 2001 From: hieuvu Date: Tue, 16 Sep 2025 13:25:10 +0700 Subject: [PATCH 3/6] Update unit and behat test. --- tests/behat/filter_embedquestion.feature | 14 ++++- ...lter_embedquestion_fillwithcorrect.feature | 10 +++- tests/external_test.php | 58 +++++++++++++------ tests/filter_test.php | 4 ++ tests/generator/lib.php | 22 +++---- 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/tests/behat/filter_embedquestion.feature b/tests/behat/filter_embedquestion.feature index f29c12d..16b2fd9 100644 --- a/tests/behat/filter_embedquestion.feature +++ b/tests/behat/filter_embedquestion.feature @@ -16,9 +16,12 @@ Feature: Add an activity and embed a question inside that activity | user | course | role | | teacher | C1 | editingteacher | | student | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | qbank | Qbank 1 | Question bank 1 | C1 | qbank1 | And the following "question categories" exist: - | contextlevel | reference | name | idnumber | - | Course | C1 | Test questions| embed | + | contextlevel | reference | name | idnumber| + | Activity module | qbank1 | Test questions | embed | And the "embedquestion" filter is "on" @javascript @@ -27,6 +30,7 @@ Feature: Add an activity and embed a question inside that activity | questioncategory | qtype | name | idnumber | | Test questions | truefalse | First question | test1 | When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (1)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" @@ -54,6 +58,7 @@ Feature: Add an activity and embed a question inside that activity | Test questions | truefalse | Q3 | test3 | | Test questions | truefalse | Q4 | test4 | When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (4)" And I set the field "id_questionidnumber" to "Choose an embeddable question from this category randomly" And I set the field "Iframe description" to "Embed question for behat testing" @@ -73,6 +78,7 @@ Feature: Add an activity and embed a question inside that activity | questioncategory | qtype | name | idnumber | | Test questions | truefalse | First question | test1 | When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (1)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" @@ -83,6 +89,7 @@ Feature: Add an activity and embed a question inside that activity And I press "id_submitbutton" # Because of the way the test page works, we need to re-select the question. Then I should see "Generate the code to embed a question" + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (1)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" @@ -95,6 +102,7 @@ Feature: Add an activity and embed a question inside that activity | questioncategory | qtype | name | idnumber | | Test questions | truefalse | First question | test1 | When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (1)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" @@ -104,6 +112,7 @@ Feature: Add an activity and embed a question inside that activity And I press "Cancel" # Because of the way the test page works, we need to re-select the question. Then I should see "Generate the code to embed a question" + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (1)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" @@ -118,6 +127,7 @@ Feature: Add an activity and embed a question inside that activity | Test questions | recordrtc | Record AV question | test1 | audio | And I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher And I expand all fieldsets + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (1)" And I set the field "id_questionidnumber" to "Record AV question" And I set the field "How the question behaves" to "Immediate feedback" diff --git a/tests/behat/filter_embedquestion_fillwithcorrect.feature b/tests/behat/filter_embedquestion_fillwithcorrect.feature index 94d0436..cd07eb3 100644 --- a/tests/behat/filter_embedquestion_fillwithcorrect.feature +++ b/tests/behat/filter_embedquestion_fillwithcorrect.feature @@ -14,9 +14,12 @@ Feature: Fill with correct feature for staff And the following "course enrolments" exist: | user | course | role | | teacher | C1 | editingteacher | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | qbank | Qbank 1 | Question bank 1 | C1 | qbank1 | And the following "question categories" exist: - | contextlevel | reference | name | idnumber | - | Course | C1 | Test questions| embed | + | contextlevel | reference | name | idnumber| + | Activity module | qbank1 | Test questions | embed | And the following "questions" exist: | questioncategory | qtype | name | idnumber | | Test questions | truefalse | First question | test1 | @@ -26,6 +29,7 @@ Feature: Fill with correct feature for staff @javascript Scenario: Teacher can see and use the Fill with correct link When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (2)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" @@ -43,6 +47,7 @@ Feature: Fill with correct feature for staff @javascript Scenario: Teacher can not see the Fill with correct link for open question When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (2)" And I set the field "id_questionidnumber" to "Second question" And I press "Embed question" @@ -52,6 +57,7 @@ Feature: Fill with correct feature for staff @javascript Scenario: Teacher can see and use the Question bank link. When I am on the "Course 1" "filter_embedquestion > test" page logged in as teacher + And I set the field "Question bank" to "Qbank 1 [qbank1]" And I set the field "Question category" to "Test questions [embed] (2)" And I set the field "id_questionidnumber" to "First question" And I press "Embed question" diff --git a/tests/external_test.php b/tests/external_test.php index 3ecfed5..957d2a5 100644 --- a/tests/external_test.php +++ b/tests/external_test.php @@ -42,11 +42,12 @@ public function test_get_sharable_question_choices_working(): void { $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']); /** @var \core_question_generator $questiongenerator */ $questiongenerator = $generator->get_plugin_generator('core_question'); $category = $questiongenerator->create_question_category([ 'name' => 'Category with idnumber', - 'contextid' => \context_course::instance($course->id)->id, + 'contextid' => \context_module::instance($qbank->cmid)->id, 'idnumber' => 'abc123', ]); @@ -63,15 +64,26 @@ public function test_get_sharable_question_choices_working(): void { ['value' => 'toad', 'label' => 'Question 2 [toad]'], ['value' => '*', 'label' => get_string('chooserandomly', 'filter_embedquestion')], ], - external::get_sharable_question_choices($course->id, 'abc123')); + external::get_sharable_question_choices($qbank->cmid, 'abc123')); } public function test_get_sharable_question_choices_no_permissions(): void { + global $DB; $this->resetAfterTest(); - $this->setGuestUser(); $this->expectException('coding_exception'); $this->expectExceptionMessage('This user is not allowed to embed questions.'); - external::get_sharable_question_choices(SITEID, 'abc123'); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $user = $generator->create_user(); + role_change_permission($DB->get_field('role', 'id', ['shortname' => 'editingteacher']), + \context_system::instance(), 'moodle/question:useall', CAP_PREVENT); + role_change_permission($DB->get_field('role', 'id', ['shortname' => 'editingteacher']), + \context_system::instance(), 'moodle/question:usemine', CAP_PREVENT); + $generator->enrol_user($user->id, $course->id, 'editingteacher'); + $this->setUser($user); + $qbank1 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id]); + + external::get_sharable_question_choices($qbank1->cmid, 'abc123'); } public function test_get_sharable_question_choices_only_user(): void { @@ -88,10 +100,11 @@ public function test_get_sharable_question_choices_only_user(): void { /** @var \core_question_generator $questiongenerator */ $questiongenerator = $generator->get_plugin_generator('core_question'); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']); $category = $questiongenerator->create_question_category([ 'name' => 'Category with idnumber', 'idnumber' => 'abc123', - 'contextid' => \context_course::instance($course->id)->id, + 'contextid' => \context_module::instance($qbank->cmid)->id, ]); $this->setAdminUser(); @@ -107,7 +120,7 @@ public function test_get_sharable_question_choices_only_user(): void { ['value' => '', 'label' => 'Choose...'], ['value' => 'frog', 'label' => 'Question 1 [frog]'], ], - external::get_sharable_question_choices($course->id, 'abc123')); + external::get_sharable_question_choices($qbank->cmid, 'abc123')); } /** @@ -115,8 +128,14 @@ public function test_get_sharable_question_choices_only_user(): void { */ public static function get_embed_code_cases(): array { return [ - ['abc123', 'toad', 'abc123/toad'], - ['A/V questions', '|---> 100%', 'A%2FV questions/%7C---> 100%25'], + ['abc123', 'toad', '', '', 'abc123/toad'], + ['abc123', 'toad', '', 'id1', 'id1/abc123/toad'], + ['abc123', 'toad', 'c1', 'id1', 'c1/id1/abc123/toad'], + ['abc123', 'toad', 'c1', '', 'c1/abc123/toad'], + ['A/V questions', '|---> 100%', '', '', 'A%2FV questions/%7C---> 100%25'], + ['A/V questions', '|---> 100%', '', 'id1', 'id1/A%2FV questions/%7C---> 100%25'], + ['A/V questions', '|---> 100%', 'c1', 'id1', 'c1/id1/A%2FV questions/%7C---> 100%25'], + ['A/V questions', '|---> 100%', 'c1', '', 'c1/A%2FV questions/%7C---> 100%25'], ]; } @@ -125,25 +144,27 @@ public static function get_embed_code_cases(): array { * * @param string $catid idnumber to use for the category. * @param string $questionid idnumber to use for the question. + * @param string $courseshortname the course shortname. + * @param string $qbankidnumber the question bank idnumber. * @param string $expectedembedid what the embed id in the output should be. * @dataProvider get_embed_code_cases */ - public function test_get_embed_code_working(string $catid, string $questionid, string $expectedembedid): void { - + public function test_get_embed_code_working(string $catid, string $questionid, + string $courseshortname, string $qbankidnumber, string $expectedembedid): void { $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); - $course = $generator->create_course(); + $course = $generator->create_course(['shortname' => $courseshortname]); /** @var \core_question_generator $questiongenerator */ $questiongenerator = $generator->get_plugin_generator('core_question'); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => $qbankidnumber]); $category = $questiongenerator->create_question_category( - ['name' => 'Category', 'idnumber' => $catid, 'contextid' => \context_course::instance($course->id)->id]); + ['name' => 'Category', 'idnumber' => $catid, 'contextid' => \context_module::instance($qbank->cmid)->id]); $questiongenerator->create_question('shortanswer', null, ['category' => $category->id, 'name' => 'Question', 'idnumber' => $questionid]); - - $embedid = new embed_id($catid, $questionid); + $embedid = new embed_id($catid, $questionid, $qbankidnumber, $courseshortname); $iframedescription = ''; $behaviour = ''; $maxmark = ''; @@ -161,7 +182,7 @@ public function test_get_embed_code_working(string $catid, string $questionid, s $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, - $generalfeedback, $rightanswer, $history, ''); + $generalfeedback, $rightanswer, $history, '', $courseshortname, $qbankidnumber); $this->assertEquals($expected, $actual); @@ -170,7 +191,7 @@ public function test_get_embed_code_working(string $catid, string $questionid, s $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback, - $rightanswer, $history, ''); + $rightanswer, $history, '', $courseshortname, $qbankidnumber); $this->assertEquals($expected, $actual); } @@ -182,10 +203,12 @@ public function test_get_embed_code_working_with_random_questions(): void { $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']); + /** @var \core_question_generator $questiongenerator */ $questiongenerator = $generator->get_plugin_generator('core_question'); $category = $questiongenerator->create_question_category( - ['name' => 'Category', 'idnumber' => 'abc123', 'contextid' => \context_course::instance($course->id)->id]); + ['name' => 'Category', 'idnumber' => 'abc123', 'contextid' => \context_module::instance($qbank->cmid)->id]); $questiongenerator->create_question('shortanswer', null, ['category' => $category->id, 'name' => 'Question1', 'idnumber' => 'toad']); @@ -263,4 +286,5 @@ public function test_is_authorized_secret_token(string $catid, string $questioni $this->assertEquals(true, token::is_authorized_secret_token($token, $embedid)); } + } diff --git a/tests/filter_test.php b/tests/filter_test.php index e2e27ed..d68ff1b 100644 --- a/tests/filter_test.php +++ b/tests/filter_test.php @@ -55,6 +55,8 @@ public static function get_cases_for_test_filter(): array { $expectedurl = new \moodle_url( '/filter/embedquestion/showquestion.php', [ + 'courseshortname' => '', + 'questionbankidnumber' => '', 'catid' => 'cat', 'qid' => 'q', 'contextid' => '1', @@ -84,6 +86,8 @@ class="filter_embedquestion-iframe" allowfullscreen loading="lazy" $expectedurl = new \moodle_url( '/filter/embedquestion/showquestion.php', [ + 'courseshortname' => '', + 'questionbankidnumber' => '', 'catid' => 'A/V questions', 'qid' => '|<--- 100%', 'contextid' => '1', diff --git a/tests/generator/lib.php b/tests/generator/lib.php index e6e0a88..6e84b35 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -73,11 +73,9 @@ public function create_embeddable_question(string $qtype, string|null $which = n $categoryrecord['idnumber'] = 'embeddablecat' . (self::$uniqueid++); } if (isset($categoryrecord['contextid'])) { - if (context::instance_by_id($categoryrecord['contextid'])->contextlevel !== CONTEXT_COURSE) { - throw new coding_exception('Categorycontextid must refer to a course context.'); + if (context::instance_by_id($categoryrecord['contextid'])->contextlevel !== CONTEXT_MODULE) { + throw new coding_exception('Categorycontextid must refer to a module context.'); } - } else { - $categoryrecord['contextid'] = context_course::instance(SITEID)->id; } $category = $this->questiongenerator->create_question_category($categoryrecord); $overrides['category'] = $category->id; @@ -113,8 +111,9 @@ public function get_embed_id_and_context(stdClass $question): array { } $context = context::instance_by_id($category->contextid); - if ($context->contextlevel !== CONTEXT_COURSE) { - throw new coding_exception('Categorycontextid must refer to a course context.'); + + if ($context->contextlevel !== CONTEXT_MODULE) { + throw new coding_exception('Categorycontextid must refer to a module context.'); } return [new embed_id($category->idnumber, $question->idnumber), $context]; @@ -145,6 +144,8 @@ public function get_embed_code(stdClass $question) { $fakeformdata = (object) [ 'categoryidnumber' => $embedid->categoryidnumber, 'questionidnumber' => $embedid->questionidnumber, + 'questionbankidnumber' => $embedid->questionbankidnumber, + 'courseshortname' => $embedid->courseshortname, ]; return question_options::get_embed_from_form_options($fakeformdata); } @@ -167,17 +168,12 @@ public function create_attempt_at_embedded_question(stdClass $question, $isfinish = true): attempt { global $USER, $CFG; - [$embedid, $coursecontext] = $this->get_embed_id_and_context($question); + [$embedid, $qbankcontext] = $this->get_embed_id_and_context($question); if ($attemptcontext) { - if ($attemptcontext->id !== $coursecontext->id && - $attemptcontext->get_parent_context()->id !== $coursecontext->id) { - throw new coding_exception('The attempt context must either be the course ' . - 'context where the question is, or one of the activities in that course.'); - } $context = $attemptcontext; } else { - $context = $coursecontext; + $context = $qbankcontext; } if ($pagename) { $pn = explode(':', $pagename); From 2fc16c985c2746a57e91623147406574754ac051 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Tue, 16 Sep 2025 13:27:54 +0700 Subject: [PATCH 4/6] Update CI and changes.md --- .github/workflows/ci.yml | 6 +++--- changes.md | 9 +++++++++ version.php | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 670dd34..f3b9890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: services: postgres: - image: postgres:14 + image: postgres:15 env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -30,8 +30,8 @@ jobs: fail-fast: false matrix: include: - - { php: '8.2', moodle-branch: MOODLE_404_STABLE, database: mariadb } - - { php: '8.3', moodle-branch: MOODLE_405_STABLE, database: pgsql } + - { php: '8.2', moodle-branch: MOODLE_500_STABLE, database: mariadb } + - { php: '8.3', moodle-branch: main, database: pgsql } steps: - name: Check out repository code diff --git a/changes.md b/changes.md index fae6f79..c1bffea 100644 --- a/changes.md +++ b/changes.md @@ -1,5 +1,14 @@ # Change log for the embed questions filter +## Changes in 2.4 + +* This version works with Moodle 5.0+ +* Add switch question bank dialog to the embedded question UI, so that teachers can + select question bank from any course they have access to. +* Add user preference to control whether the embedded question UI to select default question bank + from current course + + ## Changes in 2.3 * This version works with Moodle 4.5. diff --git a/version.php b/version.php index 2cb36a0..f656be6 100644 --- a/version.php +++ b/version.php @@ -24,10 +24,10 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025050102; +$plugin->version = 2025091600; $plugin->requires = 2024042200; // Requires Moodle 4.4. $plugin->component = 'filter_embedquestion'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = '2.3 for Moodle 4.4+'; +$plugin->release = '2.4 for Moodle 5.0+'; $plugin->outestssufficient = true; From c769e57728d0027bc79247f97001ed37e30a515d Mon Sep 17 00:00:00 2001 From: hieuvu Date: Fri, 3 Oct 2025 09:46:05 +0700 Subject: [PATCH 5/6] Fix unit test failed --- classes/privacy/provider.php | 1 + lang/en/filter_embedquestion.php | 1 - tests/provider_test.php | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 106d505..8a8d6f6 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -30,6 +30,7 @@ class provider implements \core_privacy\local\metadata\provider, #[\Override] public static function get_metadata(collection $collection): collection { $collection->add_user_preference('filter_embedquestion_userdefaultqbank', 'privacy:preference:defaultqbank'); + return $collection; } #[\Override] diff --git a/lang/en/filter_embedquestion.php b/lang/en/filter_embedquestion.php index e78aa33..2c5565d 100644 --- a/lang/en/filter_embedquestion.php +++ b/lang/en/filter_embedquestion.php @@ -72,7 +72,6 @@ $string['notyourattempt'] = 'This is not your attempt.'; $string['pluginname'] = 'Embed questions'; $string['previousattempts'] = 'Previous attempts'; -$string['privacy:metadata'] = 'The Embed questions filter does not store any personal data.'; $string['privacy:preference:defaultqbank'] = 'Stores default qbank mapping from course ID to selected question bank (JSON-encoded).'; $string['questionbank'] = 'Question bank'; $string['questionidnumber'] = 'Question id number'; diff --git a/tests/provider_test.php b/tests/provider_test.php index ead7384..4a9e7b3 100644 --- a/tests/provider_test.php +++ b/tests/provider_test.php @@ -39,8 +39,11 @@ public function test_export_user_preferences(): void { $user = $this->getDataGenerator()->create_user(); $this->setUser($user); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']); // Simulate saved user preference JSON. - $example = ['1' => '2', '2' => '3']; + $example = [$course->id => $qbank->id]; set_user_preference('filter_embedquestion_userdefaultqbank', json_encode($example), $user->id); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); From 9e7b2a8d38bf33f90a18f31fc63ff270d84e6de6 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Wed, 15 Oct 2025 14:08:36 +0700 Subject: [PATCH 6/6] Fix code review --- .../modal_embedquestion_question_bank.min.js | 2 +- ...dal_embedquestion_question_bank.min.js.map | 2 +- amd/build/questionid_choice_updater.min.js | 2 +- .../questionid_choice_updater.min.js.map | 2 +- amd/src/modal_embedquestion_question_bank.js | 13 ++-- amd/src/questionid_choice_updater.js | 18 ++--- classes/attempt.php | 19 +++-- classes/external.php | 43 +++++------ ....php => get_sharable_category_choices.php} | 6 +- classes/form/embed_options_form.php | 71 +++++++++---------- classes/question_options.php | 2 +- classes/utils.php | 57 +++++++++------ db/services.php | 4 +- lang/en/filter_embedquestion.php | 1 + lib.php | 15 ++++ templates/switch_question_bank.mustache | 2 +- testhelper.php | 2 - tests/external_test.php | 35 ++++----- tests/utils_test.php | 8 +-- version.php | 2 +- 20 files changed, 163 insertions(+), 143 deletions(-) rename classes/external/{get_sharable_categories_choices.php => get_sharable_category_choices.php} (93%) diff --git a/amd/build/modal_embedquestion_question_bank.min.js b/amd/build/modal_embedquestion_question_bank.min.js index a07a77c..7b39443 100644 --- a/amd/build/modal_embedquestion_question_bank.min.js +++ b/amd/build/modal_embedquestion_question_bank.min.js @@ -1,3 +1,3 @@ -define("filter_embedquestion/modal_embedquestion_question_bank",["exports","mod_quiz/add_question_modal","core/fragment","core/str","core/form-autocomplete"],(function(_exports,_add_question_modal,Fragment,_str,_formAutocomplete){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=_exports.ModalEmbedQuestionQuestionBank=void 0,_add_question_modal=_interopRequireDefault(_add_question_modal),Fragment=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete);const SELECTORS={SWITCH_TO_OTHER_BANK:'button[data-action="switch-question-bank"]',BANK_SEARCH:"#searchbanks",NEW_BANKMOD_ID:"data-newmodid",ANCHOR:"a[href]",SORTERS:".sorters",GO_BACK_BUTTON:'button[data-action="go-back"]'};class ModalEmbedQuestionQuestionBank extends _add_question_modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.courseId=modalConfig.courseId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,this.currentEditor=modalConfig.editor,super.configure(modalConfig)}show(){return this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH),super.show(this)}switchToEmbedQuestionModal(bankCmid){this.destroy();const event=new CustomEvent("tiny_embedquestion::displayDialog",{detail:{bankCmid:bankCmid,editor:this.currentEditor}});document.dispatchEvent(event)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("click",SELECTORS.ANCHOR,(e=>{const anchorElement=e.currentTarget;e.preventDefault(),this.switchToEmbedQuestionModal(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID))})),this.getModal().on("click",SELECTORS.GO_BACK_BUTTON,(e=>{e.preventDefault(),this.switchToEmbedQuestionModal(e.currentTarget.value)}))}async handleSwitchBankContentReload(Selector){var _document$querySelect;this.setTitle((0,_str.getString)("selectquestionbank","mod_quiz"));const el=document.createElement("button");el.classList.add("btn","btn-primary"),el.textContent=await(0,_str.getString)("gobacktoquiz","mod_quiz"),el.setAttribute("data-action","go-back"),el.setAttribute("value",this.bankCmId),this.setFooter(el),this.setBody(Fragment.loadFragment("filter_embedquestion","switch_question_bank",this.getContextId(),{courseid:this.courseId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");await this.getBodyPromise(),await _formAutocomplete.default.enhance(Selector,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),null===(_document$querySelect=document.querySelector(".search-banks .form-autocomplete-selection"))||void 0===_document$querySelect||_document$querySelect.classList.add("d-none");const bankSearchEl=document.querySelector(Selector);return bankSearchEl&&bankSearchEl.addEventListener("change",(e=>{const selectedValue=e.target.value;selectedValue>0&&this.switchToEmbedQuestionModal(selectedValue)})),this}}var obj,key,value;_exports.ModalEmbedQuestionQuestionBank=ModalEmbedQuestionQuestionBank,value="filter_embedquestion-question-bank",(key="TYPE")in(obj=ModalEmbedQuestionQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value;var _default={ModalEmbedQuestionQuestionBank:ModalEmbedQuestionQuestionBank,SELECTORS:SELECTORS};return _exports.default=_default,ModalEmbedQuestionQuestionBank.registerModalType(),_exports.default})); +define("filter_embedquestion/modal_embedquestion_question_bank",["exports","mod_quiz/add_question_modal","core/fragment","core/str","core/form-autocomplete"],(function(_exports,_add_question_modal,Fragment,_str,_formAutocomplete){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=_exports.ModalEmbedQuestionQuestionBank=void 0,_add_question_modal=_interopRequireDefault(_add_question_modal),Fragment=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Fragment),_formAutocomplete=_interopRequireDefault(_formAutocomplete);const SELECTORS_BANK_SEARCH="#searchbanks",SELECTORS_NEW_BANKMOD_ID="data-newmodid",SELECTORS_ANCHOR="a[href]",SELECTORS_GO_BACK_BUTTON='button[data-action="go-back"]';class ModalEmbedQuestionQuestionBank extends _add_question_modal.default{configure(modalConfig){modalConfig.large=!0,modalConfig.show=!0,modalConfig.removeOnClose=!0,this.setContextId(modalConfig.contextId),this.setAddOnPageId(modalConfig.addOnPage),this.courseId=modalConfig.courseId,this.bankCmId=modalConfig.bankCmId,this.originalTitle=modalConfig.title,this.currentEditor=modalConfig.editor,super.configure(modalConfig)}show(){return this.handleSwitchBankContentReload(SELECTORS_BANK_SEARCH),super.show(this)}fireQbankSelectedEvent(bankCmid){this.destroy();const event=new CustomEvent("filter_embedquestion:qbank_selected",{detail:{bankCmid:bankCmid,editor:this.currentEditor}});document.dispatchEvent(event)}registerEventListeners(){super.registerEventListeners(this),this.getModal().on("click",SELECTORS_ANCHOR,(e=>{const anchorElement=e.currentTarget;e.preventDefault(),this.fireQbankSelectedEvent(anchorElement.getAttribute(SELECTORS_NEW_BANKMOD_ID))})),this.getModal().on("click",SELECTORS_GO_BACK_BUTTON,(e=>{e.preventDefault(),this.fireQbankSelectedEvent(e.currentTarget.value)}))}async handleSwitchBankContentReload(Selector){var _document$querySelect;this.setTitle((0,_str.getString)("selectquestionbank","mod_quiz"));const el=document.createElement("button");el.classList.add("btn","btn-primary"),el.textContent=await(0,_str.getString)("gobacktoquiz","mod_quiz"),el.setAttribute("data-action","go-back"),el.setAttribute("value",this.bankCmId),this.setFooter(el),this.setBody(Fragment.loadFragment("filter_embedquestion","switch_question_bank",this.getContextId(),{courseid:this.courseId}));const placeholder=await(0,_str.getString)("searchbyname","mod_quiz");await this.getBodyPromise(),await _formAutocomplete.default.enhance(Selector,!1,"core_question/question_banks_datasource",placeholder,!1,!0,"",!0),null===(_document$querySelect=document.querySelector(".search-banks .form-autocomplete-selection"))||void 0===_document$querySelect||_document$querySelect.classList.add("d-none");const bankSearchEl=document.querySelector(Selector);return bankSearchEl&&bankSearchEl.addEventListener("change",(e=>{const selectedValue=e.target.value;selectedValue>0&&this.fireQbankSelectedEvent(selectedValue)})),this}}var obj,key,value;_exports.ModalEmbedQuestionQuestionBank=ModalEmbedQuestionQuestionBank,value="filter_embedquestion-question-bank",(key="TYPE")in(obj=ModalEmbedQuestionQuestionBank)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value;var _default={ModalEmbedQuestionQuestionBank:ModalEmbedQuestionQuestionBank};return _exports.default=_default,ModalEmbedQuestionQuestionBank.registerModalType(),_exports.default})); //# sourceMappingURL=modal_embedquestion_question_bank.min.js.map \ No newline at end of file diff --git a/amd/build/modal_embedquestion_question_bank.min.js.map b/amd/build/modal_embedquestion_question_bank.min.js.map index 449ce65..a011b90 100644 --- a/amd/build/modal_embedquestion_question_bank.min.js.map +++ b/amd/build/modal_embedquestion_question_bank.min.js.map @@ -1 +1 @@ -{"version":3,"file":"modal_embedquestion_question_bank.min.js","sources":["../src/modal_embedquestion_question_bank.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module filter_embedquestion/modal_embedquestion_question_bank\n * @copyright 2025 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Modal from 'mod_quiz/add_question_modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\n\nconst SELECTORS = {\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n BANK_SEARCH: '#searchbanks',\n NEW_BANKMOD_ID: 'data-newmodid',\n ANCHOR: 'a[href]',\n SORTERS: '.sorters',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n};\n\n/**\n * Class representing a modal for selecting a question bank to embed questions from.\n */\nexport class ModalEmbedQuestionQuestionBank extends Modal {\n static TYPE = 'filter_embedquestion-question-bank';\n\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n this.courseId = modalConfig.courseId;\n this.bankCmId = modalConfig.bankCmId;\n // Store the original title of the modal, so we can revert back to it once we have switched to another bank.\n this.originalTitle = modalConfig.title;\n this.currentEditor = modalConfig.editor;\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n /**\n * Show the modal and load the content for switching question banks.\n *\n * @method show\n */\n show() {\n this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH);\n return super.show(this);\n }\n\n /**\n * Switch to the embed question modal for a specific question bank.\n * This will destroy the current modal and dispatch an event to switch to the new modal.\n *\n * @param {String} bankCmid - The course module ID of the question bank to switch to.\n * @method switchToEmbedQuestionModal\n */\n switchToEmbedQuestionModal(bankCmid) {\n this.destroy();\n const event = new CustomEvent('tiny_embedquestion::displayDialog', {\n detail: {bankCmid: bankCmid, editor: this.currentEditor},\n });\n document.dispatchEvent(event);\n }\n\n /**\n * Set up all the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = e.currentTarget;\n e.preventDefault();\n this.switchToEmbedQuestionModal(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID));\n });\n\n this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {\n e.preventDefault();\n this.switchToEmbedQuestionModal(e.currentTarget.value);\n });\n }\n\n /**\n * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields.\n *\n * @param {String} Selector for the original select element.\n * @return {Promise} Modal.\n */\n async handleSwitchBankContentReload(Selector) {\n this.setTitle(getString('selectquestionbank', 'mod_quiz'));\n\n // Create a 'Go back' button and set it in the footer.\n const el = document.createElement('button');\n el.classList.add('btn', 'btn-primary');\n el.textContent = await getString('gobacktoquiz', 'mod_quiz');\n el.setAttribute('data-action', 'go-back');\n el.setAttribute('value', this.bankCmId);\n this.setFooter(el);\n\n this.setBody(\n Fragment.loadFragment(\n 'filter_embedquestion',\n 'switch_question_bank',\n this.getContextId(),\n {\n 'courseid': this.courseId,\n })\n );\n const placeholder = await getString('searchbyname', 'mod_quiz');\n await this.getBodyPromise();\n await AutoComplete.enhance(\n Selector,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true\n );\n\n // Hide the selection element as we don't need it.\n document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none');\n // Add a change listener to get the selected value.\n const bankSearchEl = document.querySelector(Selector);\n if (bankSearchEl) {\n bankSearchEl.addEventListener('change', (e) => {\n // This will be the chosen qbankCmid.\n const selectedValue = e.target.value;\n if (selectedValue > 0) {\n this.switchToEmbedQuestionModal(selectedValue);\n }\n });\n }\n return this;\n }\n}\n\nexport default {\n ModalEmbedQuestionQuestionBank,\n SELECTORS\n};\nModalEmbedQuestionQuestionBank.registerModalType();"],"names":["SELECTORS","SWITCH_TO_OTHER_BANK","BANK_SEARCH","NEW_BANKMOD_ID","ANCHOR","SORTERS","GO_BACK_BUTTON","ModalEmbedQuestionQuestionBank","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","courseId","bankCmId","originalTitle","title","currentEditor","editor","handleSwitchBankContentReload","super","this","switchToEmbedQuestionModal","bankCmid","destroy","event","CustomEvent","detail","document","dispatchEvent","registerEventListeners","getModal","on","e","anchorElement","currentTarget","preventDefault","getAttribute","value","Selector","setTitle","el","createElement","classList","add","textContent","setAttribute","setFooter","setBody","Fragment","loadFragment","getContextId","placeholder","getBodyPromise","AutoComplete","enhance","querySelector","bankSearchEl","addEventListener","selectedValue","target","registerModalType"],"mappings":"q+CA2BMA,UAAY,CACdC,qBAAsB,6CACtBC,YAAa,eACbC,eAAgB,gBAChBC,OAAQ,UACRC,QAAS,WACTC,eAAgB,uCAMPC,uCAAuCC,4BAGhDC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,gBAC3BC,SAAWR,YAAYQ,cACvBC,SAAWT,YAAYS,cAEvBC,cAAgBV,YAAYW,WAC5BC,cAAgBZ,YAAYa,aAE3Bd,UAAUC,aAQpBE,mBACSY,8BAA8BxB,UAAUE,aACtCuB,MAAMb,KAAKc,MAUtBC,2BAA2BC,eAClBC,gBACCC,MAAQ,IAAIC,YAAY,oCAAqC,CAC/DC,OAAQ,CAACJ,SAAUA,SAAUL,OAAQG,KAAKJ,iBAE9CW,SAASC,cAAcJ,OAQ3BK,+BAEUA,uBAAuBT,WAExBU,WAAWC,GAAG,QAASrC,UAAUI,QAASkC,UACrCC,cAAgBD,EAAEE,cACxBF,EAAEG,sBACGd,2BAA2BY,cAAcG,aAAa1C,UAAUG,yBAGpEiC,WAAWC,GAAG,QAASrC,UAAUM,gBAAiBgC,IACnDA,EAAEG,sBACGd,2BAA2BW,EAAEE,cAAcG,8CAUpBC,yCAC3BC,UAAS,kBAAU,qBAAsB,mBAGxCC,GAAKb,SAASc,cAAc,UAClCD,GAAGE,UAAUC,IAAI,MAAO,eACxBH,GAAGI,kBAAoB,kBAAU,eAAgB,YACjDJ,GAAGK,aAAa,cAAe,WAC/BL,GAAGK,aAAa,QAASzB,KAAKP,eACzBiC,UAAUN,SAEVO,QACDC,SAASC,aACL,uBACA,uBACA7B,KAAK8B,eACL,UACgB9B,KAAKR,kBAGvBuC,kBAAoB,kBAAU,eAAgB,kBAC9C/B,KAAKgC,uBACLC,0BAAaC,QACfhB,UACA,EACA,0CACAa,aACA,GACA,EACA,IACA,iCAIJxB,SAAS4B,cAAc,sGAA+Cb,UAAUC,IAAI,gBAE9Ea,aAAe7B,SAAS4B,cAAcjB,iBACxCkB,cACAA,aAAaC,iBAAiB,UAAWzB,UAE/B0B,cAAgB1B,EAAE2B,OAAOtB,MAC3BqB,cAAgB,QACXrC,2BAA2BqC,kBAIrCtC,qGAxHG,wDADLnB,mJA6HE,CACXA,+BAAAA,+BACAP,UAAAA,4CAEJO,+BAA+B2D"} \ No newline at end of file +{"version":3,"file":"modal_embedquestion_question_bank.min.js","sources":["../src/modal_embedquestion_question_bank.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Contain the logic for the question bank modal.\n *\n * @module filter_embedquestion/modal_embedquestion_question_bank\n * @copyright 2025 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Modal from 'mod_quiz/add_question_modal';\nimport * as Fragment from 'core/fragment';\nimport {getString} from 'core/str';\nimport AutoComplete from 'core/form-autocomplete';\n\nconst SELECTORS = {\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n BANK_SEARCH: '#searchbanks',\n NEW_BANKMOD_ID: 'data-newmodid',\n ANCHOR: 'a[href]',\n SORTERS: '.sorters',\n GO_BACK_BUTTON: 'button[data-action=\"go-back\"]',\n};\n\n/**\n * Class representing a modal for selecting a question bank to embed questions from.\n */\nexport class ModalEmbedQuestionQuestionBank extends Modal {\n static TYPE = 'filter_embedquestion-question-bank';\n\n configure(modalConfig) {\n // Add question modals are always large.\n modalConfig.large = true;\n\n // Always show on creation.\n modalConfig.show = true;\n modalConfig.removeOnClose = true;\n\n // Apply question modal configuration.\n this.setContextId(modalConfig.contextId);\n this.setAddOnPageId(modalConfig.addOnPage);\n this.courseId = modalConfig.courseId;\n this.bankCmId = modalConfig.bankCmId;\n // Store the original title of the modal, so we can revert back to it once we have switched to another bank.\n this.originalTitle = modalConfig.title;\n this.currentEditor = modalConfig.editor;\n // Apply standard configuration.\n super.configure(modalConfig);\n }\n\n /**\n * Show the modal and load the content for switching question banks.\n *\n * @method show\n */\n show() {\n this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH);\n return super.show(this);\n }\n\n /**\n * Switch to the embed question modal for a specific question bank.\n * This will destroy the current modal and dispatch an event to switch to the new modal.\n *\n * @param {String} bankCmid - The course module ID of the question bank to switch to.\n * @method fireQbankSelectedEvent\n */\n fireQbankSelectedEvent(bankCmid) {\n this.destroy();\n const event = new CustomEvent('filter_embedquestion:qbank_selected', {\n detail: {bankCmid: bankCmid, editor: this.currentEditor},\n });\n document.dispatchEvent(event);\n }\n\n /**\n * Set up all the event handling for the modal.\n *\n * @method registerEventListeners\n */\n registerEventListeners() {\n // Apply parent event listeners.\n super.registerEventListeners(this);\n\n this.getModal().on('click', SELECTORS.ANCHOR, (e) => {\n const anchorElement = e.currentTarget;\n e.preventDefault();\n this.fireQbankSelectedEvent(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID));\n });\n\n this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {\n e.preventDefault();\n this.fireQbankSelectedEvent(e.currentTarget.value);\n });\n }\n\n /**\n * Update the modal with a list of banks to switch to and enhance the standard selects to Autocomplete fields.\n *\n * @param {String} Selector for the original select element.\n * @return {Promise} Modal.\n */\n async handleSwitchBankContentReload(Selector) {\n this.setTitle(getString('selectquestionbank', 'mod_quiz'));\n\n // Create a 'Go back' button and set it in the footer.\n const el = document.createElement('button');\n el.classList.add('btn', 'btn-primary');\n el.textContent = await getString('gobacktoquiz', 'mod_quiz');\n el.setAttribute('data-action', 'go-back');\n el.setAttribute('value', this.bankCmId);\n this.setFooter(el);\n\n this.setBody(\n Fragment.loadFragment(\n 'filter_embedquestion',\n 'switch_question_bank',\n this.getContextId(),\n {\n 'courseid': this.courseId,\n })\n );\n const placeholder = await getString('searchbyname', 'mod_quiz');\n await this.getBodyPromise();\n await AutoComplete.enhance(\n Selector,\n false,\n 'core_question/question_banks_datasource',\n placeholder,\n false,\n true,\n '',\n true\n );\n\n // Hide the selection element as we don't need it.\n document.querySelector('.search-banks .form-autocomplete-selection')?.classList.add('d-none');\n // Add a change listener to get the selected value.\n const bankSearchEl = document.querySelector(Selector);\n if (bankSearchEl) {\n bankSearchEl.addEventListener('change', (e) => {\n // This will be the chosen qbankCmid.\n const selectedValue = e.target.value;\n if (selectedValue > 0) {\n this.fireQbankSelectedEvent(selectedValue);\n }\n });\n }\n return this;\n }\n}\n\nexport default {\n ModalEmbedQuestionQuestionBank,\n};\nModalEmbedQuestionQuestionBank.registerModalType();"],"names":["SELECTORS","ModalEmbedQuestionQuestionBank","Modal","configure","modalConfig","large","show","removeOnClose","setContextId","contextId","setAddOnPageId","addOnPage","courseId","bankCmId","originalTitle","title","currentEditor","editor","handleSwitchBankContentReload","super","this","fireQbankSelectedEvent","bankCmid","destroy","event","CustomEvent","detail","document","dispatchEvent","registerEventListeners","getModal","on","e","anchorElement","currentTarget","preventDefault","getAttribute","value","Selector","setTitle","el","createElement","classList","add","textContent","setAttribute","setFooter","setBody","Fragment","loadFragment","getContextId","placeholder","getBodyPromise","AutoComplete","enhance","querySelector","bankSearchEl","addEventListener","selectedValue","target","registerModalType"],"mappings":"q+CA2BMA,sBAEW,eAFXA,yBAGc,gBAHdA,iBAIM,UAJNA,yBAMc,sCAMPC,uCAAuCC,4BAGhDC,UAAUC,aAENA,YAAYC,OAAQ,EAGpBD,YAAYE,MAAO,EACnBF,YAAYG,eAAgB,OAGvBC,aAAaJ,YAAYK,gBACzBC,eAAeN,YAAYO,gBAC3BC,SAAWR,YAAYQ,cACvBC,SAAWT,YAAYS,cAEvBC,cAAgBV,YAAYW,WAC5BC,cAAgBZ,YAAYa,aAE3Bd,UAAUC,aAQpBE,mBACSY,8BAA8BlB,uBAC5BmB,MAAMb,KAAKc,MAUtBC,uBAAuBC,eACdC,gBACCC,MAAQ,IAAIC,YAAY,sCAAuC,CACjEC,OAAQ,CAACJ,SAAUA,SAAUL,OAAQG,KAAKJ,iBAE9CW,SAASC,cAAcJ,OAQ3BK,+BAEUA,uBAAuBT,WAExBU,WAAWC,GAAG,QAAS/B,kBAAmBgC,UACrCC,cAAgBD,EAAEE,cACxBF,EAAEG,sBACGd,uBAAuBY,cAAcG,aAAapC,mCAGtD8B,WAAWC,GAAG,QAAS/B,0BAA2BgC,IACnDA,EAAEG,sBACGd,uBAAuBW,EAAEE,cAAcG,8CAUhBC,yCAC3BC,UAAS,kBAAU,qBAAsB,mBAGxCC,GAAKb,SAASc,cAAc,UAClCD,GAAGE,UAAUC,IAAI,MAAO,eACxBH,GAAGI,kBAAoB,kBAAU,eAAgB,YACjDJ,GAAGK,aAAa,cAAe,WAC/BL,GAAGK,aAAa,QAASzB,KAAKP,eACzBiC,UAAUN,SAEVO,QACDC,SAASC,aACL,uBACA,uBACA7B,KAAK8B,eACL,UACgB9B,KAAKR,kBAGvBuC,kBAAoB,kBAAU,eAAgB,kBAC9C/B,KAAKgC,uBACLC,0BAAaC,QACfhB,UACA,EACA,0CACAa,aACA,GACA,EACA,IACA,iCAIJxB,SAAS4B,cAAc,sGAA+Cb,UAAUC,IAAI,gBAE9Ea,aAAe7B,SAAS4B,cAAcjB,iBACxCkB,cACAA,aAAaC,iBAAiB,UAAWzB,UAE/B0B,cAAgB1B,EAAE2B,OAAOtB,MAC3BqB,cAAgB,QACXrC,uBAAuBqC,kBAIjCtC,qGAxHG,wDADLnB,mJA6HE,CACXA,+BAAAA,iEAEJA,+BAA+B2D"} \ No newline at end of file diff --git a/amd/build/questionid_choice_updater.min.js b/amd/build/questionid_choice_updater.min.js index ad66386..348dea8 100644 --- a/amd/build/questionid_choice_updater.min.js +++ b/amd/build/questionid_choice_updater.min.js @@ -6,6 +6,6 @@ * @copyright 2018 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("filter_embedquestion/questionid_choice_updater",["jquery","core/ajax","core/str","core/notification","core_user/repository"],(function($,Ajax,Str,Notification,UserRepository){var t={init:function(defaultQbank){$("select#id_qbankcmid").on("change",t.qbankChanged),$("select#id_categoryidnumber").on("change",t.categoryChanged),t.lastQbank=$("select#id_qbankcmid").val(),t.lastCategory=$("select#id_categoryidnumber").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception),defaultQbank&&$("select#id_qbankcmid").val(defaultQbank).trigger("change")},lastCategory:null,lastQbank:null,categoryChanged:function(){M.util.js_pending("filter_embedquestion-get_questions"),t.lastCategory=$("select#id_categoryidnumber").val(),""===t.lastCategory?t.updateChoices([]):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_question_choices",args:{cmid:t.lastQbank,categoryidnumber:t.lastCategory}}])[0].done(t.updateChoices),$("select#id_questionidnumber").attr("disabled",!1))},qbankChanged:function(){if($("select#id_qbankcmid").val()!==t.lastQbank){M.util.js_pending("filter_embedquestion-get_categories"),t.lastQbank=$("select#id_qbankcmid").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception);var prefKey="filter_embedquestion_userdefaultqbank",courseId=document.querySelector('input[name="courseid"]').value,courseShortname=document.querySelector('input[name="courseshortname"]').value;""!==courseShortname&&null!==courseShortname||UserRepository.getUserPreference(prefKey).then((current=>{let prefs=current?JSON.parse(current):{};return prefs[courseId]=t.lastQbank,UserRepository.setUserPreference(prefKey,JSON.stringify(prefs))})).catch(Notification.exception),""===$("select#id_qbankcmid").val()?(t.updateCategories([]),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([])):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_categories_choices",args:{cmid:t.lastQbank}}])[0].done(t.updateCategories),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([]))}},updateCategories:function(response){var select=$("select#id_categoryidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_categories")},updateChoices:function(response){var select=$("select#id_questionidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_questions")}};return t})); +define("filter_embedquestion/questionid_choice_updater",["jquery","core/ajax","core/str","core/notification","core_user/repository"],(function($,Ajax,Str,Notification,UserRepository){var t={init:function(defaultQbankCmid){$("select#id_qbankcmid").on("change",t.qbankChanged),$("select#id_categoryidnumber").on("change",t.categoryChanged),t.lastQbank=$("select#id_qbankcmid").val(),t.lastCategory=$("select#id_categoryidnumber").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception),defaultQbankCmid&&$("select#id_qbankcmid").val(defaultQbankCmid).trigger("change")},lastCategory:null,lastQbank:null,categoryChanged:function(){M.util.js_pending("filter_embedquestion-get_questions"),t.lastCategory=$("select#id_categoryidnumber").val(),""===t.lastCategory?t.updateChoices([]):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_question_choices",args:{cmid:t.lastQbank,categoryidnumber:t.lastCategory}}])[0].then(t.updateChoices).catch(Notification.exception),$("select#id_questionidnumber").attr("disabled",!1))},qbankChanged:function(){if($("select#id_qbankcmid").val()!==t.lastQbank){M.util.js_pending("filter_embedquestion-get_categories"),t.lastQbank=$("select#id_qbankcmid").val();var selectedText=$("#id_qbankcmid option:selected").text();Str.get_string("currentbank","mod_quiz",selectedText).then((function(string){$("#id_questionheadercontainer h5").text(string)})).catch(Notification.exception);var prefKey="filter_embedquestion_userdefaultqbank",courseId=document.querySelector('input[name="courseid"]').value;document.querySelector('input[name="issamecourse"]').value&&UserRepository.getUserPreference(prefKey).then((current=>{let prefs=current?JSON.parse(current):{};return prefs[courseId]=t.lastQbank,UserRepository.setUserPreference(prefKey,JSON.stringify(prefs))})).catch(Notification.exception),""===$("select#id_qbankcmid").val()?(t.updateCategories([]),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([])):(Ajax.call([{methodname:"filter_embedquestion_get_sharable_category_choices",args:{cmid:t.lastQbank}}])[0].then(t.updateCategories).catch(Notification.exception),M.util.js_pending("filter_embedquestion-get_questions"),t.updateChoices([]))}},updateCategories:function(response){var select=$("select#id_categoryidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_categories")},updateChoices:function(response){var select=$("select#id_questionidnumber");select.empty(),$(response).each((function(index,option){select.append('")})),M.util.js_complete("filter_embedquestion-get_questions")}};return t})); //# sourceMappingURL=questionid_choice_updater.min.js.map \ No newline at end of file diff --git a/amd/build/questionid_choice_updater.min.js.map b/amd/build/questionid_choice_updater.min.js.map index bae3601..75d85c2 100644 --- a/amd/build/questionid_choice_updater.min.js.map +++ b/amd/build/questionid_choice_updater.min.js.map @@ -1 +1 @@ -{"version":3,"file":"questionid_choice_updater.min.js","sources":["../src/questionid_choice_updater.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * The module provides autocomplete for the question idnumber form field.\n *\n * @module filter_embedquestion/questionid_choice_updater\n * @package filter_embedquestion\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repository'],\n function($, Ajax, Str, Notification, UserRepository) {\n var t = {\n /**\n * Initialise the handling.\n *\n * @param {string} defaultQbank - The default question bank to select, if any.\n */\n init: function(defaultQbank) {\n $('select#id_qbankcmid').on('change', t.qbankChanged);\n $('select#id_categoryidnumber').on('change', t.categoryChanged);\n\n t.lastQbank = $('select#id_qbankcmid').val();\n t.lastCategory = $('select#id_categoryidnumber').val();\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n if (defaultQbank) {\n // If a default question bank is set, we need to trigger the change event to load the categories.\n $('select#id_qbankcmid').val(defaultQbank).trigger('change');\n }\n },\n\n /**\n * Used to track when the category really changes.\n */\n lastCategory: null,\n /**\n * Used to track when the question bank really changes.\n */\n lastQbank: null,\n\n /**\n * Source of data for Ajax element.\n */\n categoryChanged: function() {\n M.util.js_pending('filter_embedquestion-get_questions');\n t.lastCategory = $('select#id_categoryidnumber').val();\n if (t.lastCategory === '') {\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_question_choices',\n args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory},\n }])[0].done(t.updateChoices);\n $('select#id_questionidnumber').attr('disabled', false);\n }\n },\n\n /**\n * Source of data for Ajax element.\n */\n qbankChanged: function() {\n if ($('select#id_qbankcmid').val() === t.lastQbank) {\n return;\n }\n M.util.js_pending('filter_embedquestion-get_categories');\n t.lastQbank = $('select#id_qbankcmid').val();\n // Update the heading immediately when selection changes.\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n var prefKey = 'filter_embedquestion_userdefaultqbank';\n var courseId = document.querySelector('input[name=\"courseid\"]').value;\n var courseShortname = document.querySelector('input[name=\"courseshortname\"]').value;\n if (courseShortname === '' || courseShortname === null) {\n UserRepository.getUserPreference(prefKey).then(current => {\n let prefs = current ? JSON.parse(current) : {};\n prefs[courseId] = t.lastQbank;\n return UserRepository.setUserPreference(prefKey, JSON.stringify(prefs));\n }).catch(Notification.exception);\n }\n if ($('select#id_qbankcmid').val() === '') {\n t.updateCategories([]);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_categories_choices',\n args: {cmid: t.lastQbank}\n }])[0].done(t.updateCategories);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n }\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateCategories: function(response) {\n var select = $('select#id_categoryidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_categories');\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateChoices: function(response) {\n var select = $('select#id_questionidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_questions');\n }\n };\n return t;\n});\n"],"names":["define","$","Ajax","Str","Notification","UserRepository","t","init","defaultQbank","on","qbankChanged","categoryChanged","lastQbank","val","lastCategory","selectedText","text","get_string","then","string","catch","exception","trigger","M","util","js_pending","updateChoices","call","methodname","args","cmid","categoryidnumber","done","attr","prefKey","courseId","document","querySelector","value","courseShortname","getUserPreference","current","prefs","JSON","parse","setUserPreference","stringify","updateCategories","response","select","empty","each","index","option","append","label","js_complete"],"mappings":";;;;;;;;AAwBAA,wDAAO,CAAC,SAAU,YAAa,WAAY,oBAAqB,yBACxD,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,oBACrCC,EAAI,CAMJC,KAAM,SAASC,cACXP,EAAE,uBAAuBQ,GAAG,SAAUH,EAAEI,cACxCT,EAAE,8BAA8BQ,GAAG,SAAUH,EAAEK,iBAE/CL,EAAEM,UAAYX,EAAE,uBAAuBY,MACvCP,EAAEQ,aAAeb,EAAE,8BAA8BY,UAC7CE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,WACtBb,cAEAP,EAAE,uBAAuBY,IAAIL,cAAcc,QAAQ,WAO3DR,aAAc,KAIdF,UAAW,KAKXD,gBAAiB,WACbY,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEQ,aAAeb,EAAE,8BAA8BY,MAC1B,KAAnBP,EAAEQ,aACFR,EAAEoB,cAAc,KAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,KAAMxB,EAAEM,UAAWmB,iBAAkBzB,EAAEQ,iBAC9C,GAAGkB,KAAK1B,EAAEoB,eACdzB,EAAE,8BAA8BgC,KAAK,YAAY,KAOzDvB,aAAc,cACNT,EAAE,uBAAuBY,QAAUP,EAAEM,WAGzCW,EAAEC,KAAKC,WAAW,uCAClBnB,EAAEM,UAAYX,EAAE,uBAAuBY,UAEnCE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,eACtBa,QAAU,wCACVC,SAAWC,SAASC,cAAc,0BAA0BC,MAC5DC,gBAAkBH,SAASC,cAAc,iCAAiCC,MACtD,KAApBC,iBAA8C,OAApBA,iBAC1BlC,eAAemC,kBAAkBN,SAAShB,MAAKuB,cACvCC,MAAQD,QAAUE,KAAKC,MAAMH,SAAW,UAC5CC,MAAMP,UAAY7B,EAAEM,UACbP,eAAewC,kBAAkBX,QAASS,KAAKG,UAAUJ,WACjEtB,MAAMhB,aAAaiB,WAEa,KAAnCpB,EAAE,uBAAuBY,OACzBP,EAAEyC,iBAAiB,IACnBxB,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,MAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,uDACZC,KAAM,CAACC,KAAMxB,EAAEM,cACf,GAAGoB,KAAK1B,EAAEyC,kBACdxB,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,OASxBqB,iBAAkB,SAASC,cACnBC,OAAShD,EAAE,8BAEfgD,OAAOC,QACPjD,EAAE+C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOf,MAAQ,KAAOe,OAAOE,MAAQ,gBAE3EhC,EAAEC,KAAKgC,YAAY,wCAQvB9B,cAAe,SAASsB,cAChBC,OAAShD,EAAE,8BAEfgD,OAAOC,QACPjD,EAAE+C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOf,MAAQ,KAAOe,OAAOE,MAAQ,gBAE3EhC,EAAEC,KAAKgC,YAAY,+CAGpBlD"} \ No newline at end of file +{"version":3,"file":"questionid_choice_updater.min.js","sources":["../src/questionid_choice_updater.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * The module provides autocomplete for the question idnumber form field.\n *\n * @module filter_embedquestion/questionid_choice_updater\n * @package filter_embedquestion\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repository'],\n function($, Ajax, Str, Notification, UserRepository) {\n var t = {\n /**\n * Initialise the handling.\n *\n * @param {string} defaultQbankCmid - The default question bank to select, if any.\n */\n init: function(defaultQbankCmid) {\n $('select#id_qbankcmid').on('change', t.qbankChanged);\n $('select#id_categoryidnumber').on('change', t.categoryChanged);\n\n t.lastQbank = $('select#id_qbankcmid').val();\n t.lastCategory = $('select#id_categoryidnumber').val();\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n if (defaultQbankCmid) {\n // If a default question bank is set, we need to trigger the change event to load the categories.\n $('select#id_qbankcmid').val(defaultQbankCmid).trigger('change');\n }\n },\n\n /**\n * Used to track when the category really changes.\n */\n lastCategory: null,\n /**\n * Used to track when the question bank really changes.\n */\n lastQbank: null,\n\n /**\n * Source of data for Ajax element.\n */\n categoryChanged: function() {\n M.util.js_pending('filter_embedquestion-get_questions');\n t.lastCategory = $('select#id_categoryidnumber').val();\n if (t.lastCategory === '') {\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_question_choices',\n args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory},\n }])[0].then(t.updateChoices).catch(Notification.exception);\n $('select#id_questionidnumber').attr('disabled', false);\n }\n },\n\n /**\n * Source of data for Ajax element.\n */\n qbankChanged: function() {\n if ($('select#id_qbankcmid').val() === t.lastQbank) {\n return;\n }\n M.util.js_pending('filter_embedquestion-get_categories');\n t.lastQbank = $('select#id_qbankcmid').val();\n // Update the heading immediately when selection changes.\n var selectedText = $('#id_qbankcmid option:selected').text();\n Str.get_string('currentbank', 'mod_quiz', selectedText)\n .then(function(string) {\n $('#id_questionheadercontainer h5').text(string);\n return;\n }).catch(Notification.exception);\n var prefKey = 'filter_embedquestion_userdefaultqbank';\n var courseId = document.querySelector('input[name=\"courseid\"]').value;\n var isSameCourse = document.querySelector('input[name=\"issamecourse\"]').value;\n if (isSameCourse) {\n UserRepository.getUserPreference(prefKey).then(current => {\n let prefs = current ? JSON.parse(current) : {};\n prefs[courseId] = t.lastQbank;\n return UserRepository.setUserPreference(prefKey, JSON.stringify(prefs));\n }).catch(Notification.exception);\n }\n if ($('select#id_qbankcmid').val() === '') {\n t.updateCategories([]);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n } else {\n Ajax.call([{\n methodname: 'filter_embedquestion_get_sharable_category_choices',\n args: {cmid: t.lastQbank}\n }])[0].then(t.updateCategories).catch(Notification.exception);\n M.util.js_pending('filter_embedquestion-get_questions');\n t.updateChoices([]);\n }\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateCategories: function(response) {\n var select = $('select#id_categoryidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_categories');\n },\n\n /**\n * Update the contents of the Question select with the results of the AJAX call.\n *\n * @param {Array} response - array of options, each has fields value and label.\n */\n updateChoices: function(response) {\n var select = $('select#id_questionidnumber');\n\n select.empty();\n $(response).each(function(index, option) {\n select.append('');\n });\n M.util.js_complete('filter_embedquestion-get_questions');\n }\n };\n return t;\n});\n"],"names":["define","$","Ajax","Str","Notification","UserRepository","t","init","defaultQbankCmid","on","qbankChanged","categoryChanged","lastQbank","val","lastCategory","selectedText","text","get_string","then","string","catch","exception","trigger","M","util","js_pending","updateChoices","call","methodname","args","cmid","categoryidnumber","attr","prefKey","courseId","document","querySelector","value","getUserPreference","current","prefs","JSON","parse","setUserPreference","stringify","updateCategories","response","select","empty","each","index","option","append","label","js_complete"],"mappings":";;;;;;;;AAwBAA,wDAAO,CAAC,SAAU,YAAa,WAAY,oBAAqB,yBACxD,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,oBACrCC,EAAI,CAMJC,KAAM,SAASC,kBACXP,EAAE,uBAAuBQ,GAAG,SAAUH,EAAEI,cACxCT,EAAE,8BAA8BQ,GAAG,SAAUH,EAAEK,iBAE/CL,EAAEM,UAAYX,EAAE,uBAAuBY,MACvCP,EAAEQ,aAAeb,EAAE,8BAA8BY,UAC7CE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,WACtBb,kBAEAP,EAAE,uBAAuBY,IAAIL,kBAAkBc,QAAQ,WAO/DR,aAAc,KAIdF,UAAW,KAKXD,gBAAiB,WACbY,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEQ,aAAeb,EAAE,8BAA8BY,MAC1B,KAAnBP,EAAEQ,aACFR,EAAEoB,cAAc,KAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,KAAMxB,EAAEM,UAAWmB,iBAAkBzB,EAAEQ,iBAC9C,GAAGI,KAAKZ,EAAEoB,eAAeN,MAAMhB,aAAaiB,WAChDpB,EAAE,8BAA8B+B,KAAK,YAAY,KAOzDtB,aAAc,cACNT,EAAE,uBAAuBY,QAAUP,EAAEM,WAGzCW,EAAEC,KAAKC,WAAW,uCAClBnB,EAAEM,UAAYX,EAAE,uBAAuBY,UAEnCE,aAAed,EAAE,iCAAiCe,OACtDb,IAAIc,WAAW,cAAe,WAAYF,cACrCG,MAAK,SAASC,QACXlB,EAAE,kCAAkCe,KAAKG,WAE1CC,MAAMhB,aAAaiB,eACtBY,QAAU,wCACVC,SAAWC,SAASC,cAAc,0BAA0BC,MAC7CF,SAASC,cAAc,8BAA8BC,OAEpEhC,eAAeiC,kBAAkBL,SAASf,MAAKqB,cACvCC,MAAQD,QAAUE,KAAKC,MAAMH,SAAW,UAC5CC,MAAMN,UAAY5B,EAAEM,UACbP,eAAesC,kBAAkBV,QAASQ,KAAKG,UAAUJ,WACjEpB,MAAMhB,aAAaiB,WAEa,KAAnCpB,EAAE,uBAAuBY,OACzBP,EAAEuC,iBAAiB,IACnBtB,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,MAEhBxB,KAAKyB,KAAK,CAAC,CACPC,WAAY,qDACZC,KAAM,CAACC,KAAMxB,EAAEM,cACf,GAAGM,KAAKZ,EAAEuC,kBAAkBzB,MAAMhB,aAAaiB,WACnDE,EAAEC,KAAKC,WAAW,sCAClBnB,EAAEoB,cAAc,OASxBmB,iBAAkB,SAASC,cACnBC,OAAS9C,EAAE,8BAEf8C,OAAOC,QACP/C,EAAE6C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOd,MAAQ,KAAOc,OAAOE,MAAQ,gBAE3E9B,EAAEC,KAAK8B,YAAY,wCAQvB5B,cAAe,SAASoB,cAChBC,OAAS9C,EAAE,8BAEf8C,OAAOC,QACP/C,EAAE6C,UAAUG,MAAK,SAASC,MAAOC,QAC7BJ,OAAOK,OAAO,kBAAoBD,OAAOd,MAAQ,KAAOc,OAAOE,MAAQ,gBAE3E9B,EAAEC,KAAK8B,YAAY,+CAGpBhD"} \ No newline at end of file diff --git a/amd/src/modal_embedquestion_question_bank.js b/amd/src/modal_embedquestion_question_bank.js index 35ed7fe..97353a6 100644 --- a/amd/src/modal_embedquestion_question_bank.js +++ b/amd/src/modal_embedquestion_question_bank.js @@ -75,11 +75,11 @@ export class ModalEmbedQuestionQuestionBank extends Modal { * This will destroy the current modal and dispatch an event to switch to the new modal. * * @param {String} bankCmid - The course module ID of the question bank to switch to. - * @method switchToEmbedQuestionModal + * @method fireQbankSelectedEvent */ - switchToEmbedQuestionModal(bankCmid) { + fireQbankSelectedEvent(bankCmid) { this.destroy(); - const event = new CustomEvent('tiny_embedquestion::displayDialog', { + const event = new CustomEvent('filter_embedquestion:qbank_selected', { detail: {bankCmid: bankCmid, editor: this.currentEditor}, }); document.dispatchEvent(event); @@ -97,12 +97,12 @@ export class ModalEmbedQuestionQuestionBank extends Modal { this.getModal().on('click', SELECTORS.ANCHOR, (e) => { const anchorElement = e.currentTarget; e.preventDefault(); - this.switchToEmbedQuestionModal(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID)); + this.fireQbankSelectedEvent(anchorElement.getAttribute(SELECTORS.NEW_BANKMOD_ID)); }); this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => { e.preventDefault(); - this.switchToEmbedQuestionModal(e.currentTarget.value); + this.fireQbankSelectedEvent(e.currentTarget.value); }); } @@ -154,7 +154,7 @@ export class ModalEmbedQuestionQuestionBank extends Modal { // This will be the chosen qbankCmid. const selectedValue = e.target.value; if (selectedValue > 0) { - this.switchToEmbedQuestionModal(selectedValue); + this.fireQbankSelectedEvent(selectedValue); } }); } @@ -164,6 +164,5 @@ export class ModalEmbedQuestionQuestionBank extends Modal { export default { ModalEmbedQuestionQuestionBank, - SELECTORS }; ModalEmbedQuestionQuestionBank.registerModalType(); \ No newline at end of file diff --git a/amd/src/questionid_choice_updater.js b/amd/src/questionid_choice_updater.js index b62b530..30ec68f 100644 --- a/amd/src/questionid_choice_updater.js +++ b/amd/src/questionid_choice_updater.js @@ -28,9 +28,9 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repos /** * Initialise the handling. * - * @param {string} defaultQbank - The default question bank to select, if any. + * @param {string} defaultQbankCmid - The default question bank to select, if any. */ - init: function(defaultQbank) { + init: function(defaultQbankCmid) { $('select#id_qbankcmid').on('change', t.qbankChanged); $('select#id_categoryidnumber').on('change', t.categoryChanged); @@ -42,9 +42,9 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repos $('#id_questionheadercontainer h5').text(string); return; }).catch(Notification.exception); - if (defaultQbank) { + if (defaultQbankCmid) { // If a default question bank is set, we need to trigger the change event to load the categories. - $('select#id_qbankcmid').val(defaultQbank).trigger('change'); + $('select#id_qbankcmid').val(defaultQbankCmid).trigger('change'); } }, @@ -69,7 +69,7 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repos Ajax.call([{ methodname: 'filter_embedquestion_get_sharable_question_choices', args: {cmid: t.lastQbank, categoryidnumber: t.lastCategory}, - }])[0].done(t.updateChoices); + }])[0].then(t.updateChoices).catch(Notification.exception); $('select#id_questionidnumber').attr('disabled', false); } }, @@ -92,8 +92,8 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repos }).catch(Notification.exception); var prefKey = 'filter_embedquestion_userdefaultqbank'; var courseId = document.querySelector('input[name="courseid"]').value; - var courseShortname = document.querySelector('input[name="courseshortname"]').value; - if (courseShortname === '' || courseShortname === null) { + var isSameCourse = document.querySelector('input[name="issamecourse"]').value; + if (isSameCourse) { UserRepository.getUserPreference(prefKey).then(current => { let prefs = current ? JSON.parse(current) : {}; prefs[courseId] = t.lastQbank; @@ -106,9 +106,9 @@ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core_user/repos t.updateChoices([]); } else { Ajax.call([{ - methodname: 'filter_embedquestion_get_sharable_categories_choices', + methodname: 'filter_embedquestion_get_sharable_category_choices', args: {cmid: t.lastQbank} - }])[0].done(t.updateCategories); + }])[0].then(t.updateCategories).catch(Notification.exception); M.util.js_pending('filter_embedquestion-get_questions'); t.updateChoices([]); } diff --git a/classes/attempt.php b/classes/attempt.php index ecaa05f..9210f37 100644 --- a/classes/attempt.php +++ b/classes/attempt.php @@ -93,22 +93,29 @@ public function __construct(embed_id $embedid, embed_location $embedlocation, $this->embedlocation = $embedlocation; $this->user = $user; $this->options = $options; - $this->category = $this->find_category($embedid->categoryidnumber, $embedid->courseshortname, - $embedid->questionbankidnumber); + + $courseid = utils::get_relevant_courseid($this->embedlocation->context); + if ($embedid->courseshortname) { + $courseid = utils::get_courseid_by_course_shortname($embedid->courseshortname); + } + $this->category = $this->find_category( + $embedid->categoryidnumber, + $courseid, + $embedid->questionbankidnumber + ); } /** * Find the category for a category idnumber, if it exists. * * @param string $categoryidnumber idnumber of the category to use. - * @param string|null $courseshortname the short name of the course, if relevant. + * @param int $courseid the id of the course question banks are being shared from. * @param string|null $qbankidnumber the idnumber of the question bank, * @return \stdClass if the category was OK. If not null and problem and problemdetails are set. */ - private function find_category(string $categoryidnumber, ?string $courseshortname = null, + private function find_category(string $categoryidnumber, int $courseid, ?string $qbankidnumber = null): ?\stdClass { - $cmid = utils::get_qbank_by_idnumber(utils::get_relevant_courseid($this->embedlocation->context), - $courseshortname, $qbankidnumber); + $cmid = utils::get_qbank_by_idnumber($courseid, $qbankidnumber); if (!$cmid || $cmid === -1) { if ($cmid === -1) { $this->problem = 'invalidquestionbank'; diff --git a/classes/external.php b/classes/external.php index e081e61..1c50202 100644 --- a/classes/external.php +++ b/classes/external.php @@ -113,8 +113,8 @@ public static function get_embed_code_parameters(): \external_function_parameter // We can't use things like PARAM_INT for things like variant, because it is // and int of '' for not set. return new \external_function_parameters([ - 'courseid' => new \external_value(PARAM_INT, - 'Course id.'), + 'cmid' => new \external_value(PARAM_INT, + 'Course module id of the question bank.'), 'categoryidnumber' => new \external_value(PARAM_RAW, 'Id number of the question category.'), 'questionidnumber' => new \external_value(PARAM_RAW, @@ -143,10 +143,6 @@ public static function get_embed_code_parameters(): \external_function_parameter 'Whether to show the response history (1/0/"") for show, hide or default.'), 'forcedlanguage' => new \external_value(PARAM_LANG, 'Whether to force the UI language of the question. Lang code or empty string.'), - 'courseshortname' => new \external_value(PARAM_RAW, - 'Course short name.', VALUE_OPTIONAL), - 'questionbankidnumber' => new \external_value(PARAM_RAW, - 'Qbank idnumber.', VALUE_OPTIONAL), ]); } @@ -172,7 +168,7 @@ public static function get_embed_code_is_allowed_from_ajax(): bool { * Given the course id, category and question idnumbers, and any display options, * return the {Q{...}Q} code needed to embed this question. * - * @param int $courseid the course id. + * @param int $cmid the course module id of the question bank. * @param string $categoryidnumber the idnumber of the question category. * @param string $questionidnumber the idnumber of the question to be embedded, or '*' to mean a question picked at random. * @param string $iframedescription the iframe description. @@ -187,21 +183,19 @@ public static function get_embed_code_is_allowed_from_ajax(): bool { * @param string $rightanswer 0, 1 or ''. * @param string $history 0, 1 or ''. * @param string $forcedlanguage moodle lang pack (e.g. 'fr') or ''. - * @param string|null $courseshortname the course shortname, optional. - * @param string|null $questionbankidnumber the question bank idnumber, optional. * * @return string the embed code. */ - public static function get_embed_code(int $courseid, string $categoryidnumber, string $questionidnumber, + public static function get_embed_code(int $cmid, string $categoryidnumber, string $questionidnumber, string $iframedescription, string $behaviour, string $maxmark, string $variant, string $correctness, string $marks, string $markdp, string $feedback, string $generalfeedback, string $rightanswer, string $history, - string $forcedlanguage, ?string $courseshortname = null, ?string $questionbankidnumber = null): string { + string $forcedlanguage): string { global $CFG; self::validate_parameters( self::get_embed_code_parameters(), [ - 'courseid' => $courseid, + 'cmid' => $cmid, 'categoryidnumber' => $categoryidnumber, 'questionidnumber' => $questionidnumber, 'iframedescription' => $iframedescription, @@ -216,18 +210,14 @@ public static function get_embed_code(int $courseid, string $categoryidnumber, s 'rightanswer' => $rightanswer, 'history' => $history, 'forcedlanguage' => $forcedlanguage, - 'courseshortname' => $courseshortname, - 'questionbankidnumber' => $questionbankidnumber, ] ); - - $cmid = utils::get_qbank_by_idnumber($courseid, $courseshortname, $questionbankidnumber); - if ($cmid === -1) { - throw new moodle_exception('invalidquestionbank', 'filter_embedquestion'); - } $context = \context_module::instance($cmid); - self::validate_context($context); // Check permissions. + self::validate_context($context); + if (!utils::has_permission($context) || !get_coursemodule_from_id('qbank', $cmid)) { + throw new moodle_exception('errornopermissions', 'filter_embedquestion'); + } require_once($CFG->libdir . '/questionlib.php'); $category = utils::get_category_by_idnumber($context, $categoryidnumber); if ($questionidnumber === '*') { @@ -237,17 +227,16 @@ public static function get_embed_code(int $courseid, string $categoryidnumber, s $question = \question_bank::load_question($questiondata->id); question_require_capability_on($question, 'use'); } - // When we get the question bank created by system in a different course, usually they don't have idnumber + // When we get the question bank created by system, usually they don't have idnumber // So we need to add '*' to questionbankidnumber to make sure the question bank can be found. - if (empty($questionbankidnumber) && $courseshortname) { - $course = get_course($courseid); - if ($courseshortname !== $course->shortname) { - $questionbankidnumber = '*'; - } + [$embedcourse, $cm] = get_course_and_cm_from_cmid($cmid, 'qbank'); + $questionbankidnumber = $cm->idnumber; + if (empty($questionbankidnumber)) { + $questionbankidnumber = '*'; } $fromform = new \stdClass(); $fromform->questionbankidnumber = $questionbankidnumber; - $fromform->courseshortname = $courseshortname; + $fromform->courseshortname = $embedcourse->shortname; $fromform->categoryidnumber = $categoryidnumber; $fromform->questionidnumber = $questionidnumber; $fromform->iframedescription = $iframedescription; diff --git a/classes/external/get_sharable_categories_choices.php b/classes/external/get_sharable_category_choices.php similarity index 93% rename from classes/external/get_sharable_categories_choices.php rename to classes/external/get_sharable_category_choices.php index 339f145..1bd3f3c 100644 --- a/classes/external/get_sharable_categories_choices.php +++ b/classes/external/get_sharable_category_choices.php @@ -31,9 +31,9 @@ * @copyright 2025 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class get_sharable_categories_choices extends external_api { +class get_sharable_category_choices extends external_api { /** - * Returns parameter types for get_sharable_categories_choices function. + * Returns parameter types for get_sharable_category_choices function. * * @return external_function_parameters Parameters */ @@ -44,7 +44,7 @@ public static function execute_parameters(): external_function_parameters { } /** - * Returns result type for get_sharable_categories_choices function. + * Returns result type for get_sharable_category_choices function. * * @return external_description Result type */ diff --git a/classes/form/embed_options_form.php b/classes/form/embed_options_form.php index e59fefe..839fb46 100644 --- a/classes/form/embed_options_form.php +++ b/classes/form/embed_options_form.php @@ -47,7 +47,7 @@ public function definition() { $mform = $this->_form; /** @var \context $context */ $context = $this->_customdata['context']; - $courseshortname = $this->_customdata['cousrseshortname'] ?? null; + $courseshortname = $this->_customdata['courseshortname'] ?? null; $defaultqbankcmid = $this->_customdata['qbankcmid'] ?? null; $embedcode = $this->_customdata['embedcode'] ?? null; @@ -59,58 +59,45 @@ public function definition() { $defaultoptions = new question_options(); $mform->addElement('hidden', 'contextid', $context->id); $mform->setType('contextid', PARAM_INT); - $mform->setType('courseshortname', PARAM_RAW); $mform->addElement('hidden', 'courseid', $context->instanceid); $mform->setType('courseid', PARAM_INT); - $mform->addElement('hidden', 'courseshortname', ''); - $mform->setType('courseshortname', PARAM_RAW); + $mform->addElement('hidden', 'issamecourse', 1); + $mform->setType('issamecourse', PARAM_INT); $mform->addElement('header', 'questionheader', get_string('whichquestion', 'filter_embedquestion')); $prefs = []; // Only load user preference if we do not have a default question bank cmid or embed code. if (!$defaultqbankcmid && !$embedcode) { - // Preference key unique to your filter. - $prefname = 'filter_embedquestion_userdefaultqbank'; // Retrieve existing preference (empty array if none). - $prefs = json_decode(get_user_preferences($prefname, '{}')); + $prefs = json_decode(get_user_preferences('filter_embedquestion_userdefaultqbank', '{}')); } - $cmid = !empty($defaultqbankcmid) ? $defaultqbankcmid : ($prefs->{$context->instanceid} ?? null); - // If we have default question bank cmid, we will use it to get the course shortname. + // If we have default question bank cmid, we will use it to get the course id. if ($cmid) { [, $cm] = get_course_and_cm_from_cmid($cmid); $cminfo = cm_info::create($cm); - $courseshortname = $cminfo->get_course()->shortname; - if ($cminfo->get_course()->id !== $context->instanceid) { - // If the course shortname is not the same as the course shortname, we need to add it to the form. - // This is to allow the course shortname to the embed code. - $mform->setDefault('courseshortname', $courseshortname); + $courseid = $cminfo->get_course()->id; + } else if ($courseshortname) { + $courseid = utils::get_courseid_by_course_shortname($courseshortname); + if ($courseid != $context->instanceid) { + $mform->setDefault('issamecourse', 0); } + } else { + $courseid = $context->instanceid; } - - $qbanks = utils::get_shareable_question_banks($context->instanceid, $courseshortname, - $this->get_user_retriction()); + $qbanks = utils::get_shareable_question_banks($courseid, $this->get_user_retriction()); $qbanksselectoptions = utils::create_select_qbank_choices($qbanks); - // Build the hidden array of question bank idnumbers. - $qbanksidnumber = array_combine( - array_keys($qbanks), - array_map(fn($q) => $q->qbankidnumber, $qbanks) - ); // If we have a default question bank cmid, we will use it to set the default value. // If the default question bank cmid is not in the list of question banks, we will add it. if ($cmid && empty($qbanksselectoptions[$cmid])) { $qbanksselectoptions[$cmid] = format_string($cminfo->name); - $qbanksidnumber[$cmid] = $cminfo->idnumber; } $mform->addElement('html', $OUTPUT->render_from_template('mod_quiz/switch_bank_header', ['currentbank' => reset($qbanksselectoptions)])); $mform->addElement('select', 'qbankcmid', get_string('questionbank', 'question'), $qbanksselectoptions); $mform->addRule('qbankcmid', null, 'required', null, 'client'); - $mform->addElement('hidden', 'qbankidnumber'); - $mform->setType('qbankidnumber', PARAM_RAW); - $mform->setDefault('qbankidnumber', json_encode($qbanksidnumber)); $mform->addElement('select', 'categoryidnumber', get_string('questioncategory', 'question'), []); @@ -226,20 +213,21 @@ public function definition_after_data() { return; } $qbankcmid = $qbankcmid[0]; + if (!$qbankcmid || $qbankcmid == -1) { + return; + } $categoryidnumber = $categoryidnumbers[0]; if ($categoryidnumber === '' || $categoryidnumber === null) { return; } - $courseshortname = $mform->getElementValue('courseshortname'); - if ($courseshortname) { - $qbanks = utils::get_shareable_question_banks($this->_customdata['context']->instanceid, - $courseshortname, $this->get_user_retriction()); - $qbanksselectoptions = utils::create_select_qbank_choices($qbanks); - $element = $mform->getElement('qbankcmid'); - // Clear the existing options, so that we can load the new ones. - $element->_options = []; - $mform->getElement('qbankcmid')->loadArray($qbanksselectoptions); - } + + [$course,] = get_course_and_cm_from_cmid($qbankcmid); + $qbanks = utils::get_shareable_question_banks($course->id, $this->get_user_retriction()); + $qbanksselectoptions = utils::create_select_qbank_choices($qbanks); + $element = $mform->getElement('qbankcmid'); + // Clear the existing options, so that we can load the new ones. + $element->_options = []; + $mform->getElement('qbankcmid')->loadArray($qbanksselectoptions); $context = \context_module::instance($qbankcmid); $mform->setDefault('qbankcmid', $qbankcmid); @@ -277,12 +265,17 @@ protected function get_user_retriction(): ?int { #[\Override] public function validation($data, $files) { $errors = parent::validation($data, $files); - if (!isset($data['qbankcmid'])) { + $qbankcontext = \context_module::instance($data['qbankcmid']); + if (!$qbankcontext) { $errors['qbankcmid'] = get_string('errorquestionbanknotfound', 'filter_embedquestion'); + return $errors; + } + if (!utils::has_permission($qbankcontext) || !get_coursemodule_from_id('qbank', $data['qbankcmid'])) { + $errors['qbankcmid'] = get_string('errornopermissions', 'filter_embedquestion'); + return $errors; } - $qbankcontext = \context_module::instance($data['qbankcmid']); - $category = utils::get_category_by_idnumber($qbankcontext, $data['categoryidnumber']); + $category = utils::get_category_by_idnumber($qbankcontext, $data['categoryidnumber']); $questiondata = false; if (isset($data['questionidnumber'])) { $questiondata = utils::get_question_by_idnumber($category->id, $data['questionidnumber']); diff --git a/classes/question_options.php b/classes/question_options.php index 892f2a9..b691cba 100644 --- a/classes/question_options.php +++ b/classes/question_options.php @@ -177,7 +177,7 @@ public function add_params_to_url(\moodle_url $url): void { public static function get_embed_from_form_options(\stdClass $fromform): string { $embedid = new embed_id($fromform->categoryidnumber, $fromform->questionidnumber, - $fromform->questionbankidnumber ?? '', $fromform->courseshortname); + $fromform->questionbankidnumber ?? '', $fromform->courseshortname ?? ''); $parts = [(string) $embedid]; foreach (self::get_field_types() as $field => $type) { if (!isset($fromform->$field) || $fromform->$field === '') { diff --git a/classes/utils.php b/classes/utils.php index 27e6abd..03c7029 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -140,17 +140,15 @@ public static function get_show_url(embed_id $embedid, embed_location $embedloca /** * Find a question bank with a given idnumber in a given course. * - * @param int $currentcourseid the id of the course to look in. - * @param string|null $courseshortname the shortname of the course to look in. + * @param int $courseid the id of the course to look in. * @param string|null $qbankidnumber the idnumber of the question bank to look for. * @param int|null $userid if set, only count question banks created by this user. * @return int|null cmid or null if not found. * If there are multiple question banks in the course, and no idnumber is given, return -1 only if there is no * question bank with no idnumber created by system. */ - public static function get_qbank_by_idnumber(int $currentcourseid, ?string $courseshortname = null, - ?string $qbankidnumber = null, ?int $userid = null): ?int { - $qbanks = self::get_shareable_question_banks($currentcourseid, $courseshortname, $userid, $qbankidnumber); + public static function get_qbank_by_idnumber(int $courseid, ?string $qbankidnumber = null, ?int $userid = null): ?int { + $qbanks = self::get_shareable_question_banks($courseid, $userid, $qbankidnumber); if (empty($qbanks)) { return null; } else if (count($qbanks) === 1) { @@ -206,19 +204,17 @@ public static function get_category_by_idnumber(\context $context, string $idnum * * The list is returned in a form suitable for using in a select menu. * - * @param int $currentcourseid the id of the course to look in. - * @param string|null $courseshortname the shortname of the course to look in. - * @param int|null $userid if set, only count question banks created by this user. + * @param int $courseid the id of the course to look in. + * @param int|null $userid if set, only count question created by this user. * @param string|null $qbankidnumber if set, only count question banks with this idnumber. * @return array course module id => object with fields cmid, qbankidnumber, courseid, qbankid. */ - public static function get_shareable_question_banks(int $currentcourseid, ?string $courseshortname = null, + public static function get_shareable_question_banks(int $courseid, ?int $userid = null, ?string $qbankidnumber = null): array { global $DB; $params = [ 'modulename' => 'qbank', - 'courseshortname' => $courseshortname ?: null, - 'currentcourseid' => $courseshortname ? null : $currentcourseid, + 'courseid' => $courseid, 'contextlevel' => CONTEXT_MODULE, 'ready' => question_version_status::QUESTION_STATUS_READY, ]; @@ -256,7 +252,7 @@ public static function get_shareable_question_banks(int $currentcourseid, ?strin AND qv2.status = :ready ) JOIN {question} q ON q.id = qv.questionid - WHERE (c.shortname = :courseshortname OR c.id = :currentcourseid) + WHERE c.id = :courseid AND qc.idnumber IS NOT NULL AND qc.idnumber <> '' $idnumber @@ -365,18 +361,13 @@ public static function get_categories_with_sharable_question_choices(\context $c $params['userid'] = $userid; } $params['status'] = question_version_status::QUESTION_STATUS_READY; - $params['cmid'] = $context->instanceid; - $params['contextlevel'] = CONTEXT_MODULE; $params['modulename'] = 'qbank'; + $params['contextid'] = $context->id; $categories = $DB->get_records_sql(" SELECT qc.id, qc.name, qc.idnumber, COUNT(q.id) AS count FROM {question_categories} qc - JOIN {context} ctx ON ctx.id = qc.contextid - JOIN {course_modules} cm ON cm.id = ctx.instanceid - JOIN {modules} m ON m.id = cm.module AND m.name = :modulename - JOIN {qbank} qbank ON qbank.id = cm.instance JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id AND qbe.idnumber IS NOT NULL $creatortest JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id AND qv.version = ( @@ -386,8 +377,7 @@ public static function get_categories_with_sharable_question_choices(\context $c ) JOIN {question} q ON q.id = qv.questionid - WHERE cm.id = :cmid - AND ctx.contextlevel = :contextlevel + WHERE qc.contextid = :contextid AND qc.idnumber IS NOT NULL AND qc.idnumber <> '' GROUP BY qc.id, qc.name, qc.idnumber @@ -626,4 +616,31 @@ public static function moodle_version_is(string $operator, string $version): boo return false; } + + /** + * Get course id by course shortname. + * + * @param string $courseshortname + * @return int + */ + public static function get_courseid_by_course_shortname(string $courseshortname): int { + global $DB; + return $DB->get_field('course', 'id', ['shortname' => $courseshortname]); + } + + /** + * Check if the current user has permission to embed questions in this context. + * + * @param \context $context the context to check. + * @return bool true if the user has permission, false if not. + */ + public static function has_permission(\context $context): bool { + if (has_capability('moodle/question:useall', $context)) { + return true; + } else if (has_capability('moodle/question:usemine', $context)) { + return true; + } else { + return false; + } + } } diff --git a/db/services.php b/db/services.php index e269157..43b685b 100644 --- a/db/services.php +++ b/db/services.php @@ -34,8 +34,8 @@ 'ajax' => true, ], - 'filter_embedquestion_get_sharable_categories_choices' => [ - 'classname' => 'filter_embedquestion\external\get_sharable_categories_choices', + 'filter_embedquestion_get_sharable_category_choices' => [ + 'classname' => 'filter_embedquestion\external\get_sharable_category_choices', 'description' => 'Use by form autocomplete for selecting a sharable qbank.', 'type' => 'read', 'ajax' => true, diff --git a/lang/en/filter_embedquestion.php b/lang/en/filter_embedquestion.php index 2c5565d..6698925 100644 --- a/lang/en/filter_embedquestion.php +++ b/lang/en/filter_embedquestion.php @@ -57,6 +57,7 @@ $string['iframetitleauto'] = 'Embedded question {$a}'; $string['invalidcantfindquestionbank'] = 'We have multiple question banks in this context "{$a->contextname}", but the one you are trying to use does not exist.'; $string['invalidcategory'] = 'The category with idnumber "{$a->catid}" does not exist in "{$a->contextname}".'; +$string['invalidemptycategory'] = 'The category "{$a->catname}" in "{$a->contextname}" does not contain any embeddable questions.'; $string['invalidqbankidnumber'] = 'The question bank with idnumber "{$a->qbankidnumber}" does not exist in "{$a->contextname}".'; $string['invalidquestion'] = 'The question with idnumber "{$a->qid}" does not exist in category "{$a->catname} [{$a->catidnumber}]".'; $string['invalidquestionbank'] = 'The question bank does not exist.'; diff --git a/lib.php b/lib.php index 53a88f5..8df4c60 100644 --- a/lib.php +++ b/lib.php @@ -74,6 +74,21 @@ function filter_embedquestion_question_pluginfile($givencourse, $context, $compo send_stored_file($file, 0, 0, $forcedownload, $fileoptions); } +/** + * Build and return the output for the question bank and category chooser. + * + * @param array $args provided by the AJAX request. + * @return string html to render to the modal. + */ +function filter_embedquestion_output_fragment_switch_question_bank(array $args): string { + global $USER, $OUTPUT; + + $courseid = clean_param($args['courseid'], PARAM_INT); + $switchbankwidget = new filter_embedquestion\output\switch_question_bank($courseid, $USER->id); + + return $OUTPUT->render($switchbankwidget); +} + /** * Allow update of user preferences via AJAX. * diff --git a/templates/switch_question_bank.mustache b/templates/switch_question_bank.mustache index db00e60..ae458f9 100644 --- a/templates/switch_question_bank.mustache +++ b/templates/switch_question_bank.mustache @@ -128,4 +128,4 @@
{{#str}}otherquestionbank, core_question{{/str}}
- \ No newline at end of file + diff --git a/testhelper.php b/testhelper.php index 644fafd..aa90c11 100644 --- a/testhelper.php +++ b/testhelper.php @@ -68,8 +68,6 @@ \filter_embedquestion\event\token_created::create( ['context' => $context, 'objectid' => $question->id])->trigger(); } - $fromform->questionbankidnumber = ''; - $fromform->courseshortname = ''; $embedcode = question_options::get_embed_from_form_options($fromform); echo html_writer::tag('p', 'Code to embed the question: ' . s($embedcode)); diff --git a/tests/external_test.php b/tests/external_test.php index 957d2a5..dd0fe65 100644 --- a/tests/external_test.php +++ b/tests/external_test.php @@ -128,14 +128,14 @@ public function test_get_sharable_question_choices_only_user(): void { */ public static function get_embed_code_cases(): array { return [ - ['abc123', 'toad', '', '', 'abc123/toad'], + ['abc123', 'toad', '', '', '*/abc123/toad'], ['abc123', 'toad', '', 'id1', 'id1/abc123/toad'], ['abc123', 'toad', 'c1', 'id1', 'c1/id1/abc123/toad'], - ['abc123', 'toad', 'c1', '', 'c1/abc123/toad'], - ['A/V questions', '|---> 100%', '', '', 'A%2FV questions/%7C---> 100%25'], + ['abc123', 'toad', 'c1', '', 'c1/*/abc123/toad'], + ['A/V questions', '|---> 100%', '', '', '*/A%2FV questions/%7C---> 100%25'], ['A/V questions', '|---> 100%', '', 'id1', 'id1/A%2FV questions/%7C---> 100%25'], ['A/V questions', '|---> 100%', 'c1', 'id1', 'c1/id1/A%2FV questions/%7C---> 100%25'], - ['A/V questions', '|---> 100%', 'c1', '', 'c1/A%2FV questions/%7C---> 100%25'], + ['A/V questions', '|---> 100%', 'c1', '', 'c1/*/A%2FV questions/%7C---> 100%25'], ]; } @@ -164,6 +164,9 @@ public function test_get_embed_code_working(string $catid, string $questionid, $questiongenerator->create_question('shortanswer', null, ['category' => $category->id, 'name' => 'Question', 'idnumber' => $questionid]); + if (!$qbankidnumber) { + $qbankidnumber = '*'; + } $embedid = new embed_id($catid, $questionid, $qbankidnumber, $courseshortname); $iframedescription = ''; $behaviour = ''; @@ -179,19 +182,19 @@ public function test_get_embed_code_working(string $catid, string $questionid, $token = token::make_secret_token($embedid); $expected = '{Q{' . $expectedembedid . '|' . $token . '}Q}'; - $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, + $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, - $generalfeedback, $rightanswer, $history, '', $courseshortname, $qbankidnumber); + $generalfeedback, $rightanswer, $history, ''); $this->assertEquals($expected, $actual); $behaviour = 'immediatefeedback'; $expected = '{Q{' . $expectedembedid . '|behaviour=' . $behaviour . '|' . $token . '}Q}'; - $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, + $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback, - $rightanswer, $history, '', $courseshortname, $qbankidnumber); + $rightanswer, $history, ''); $this->assertEquals($expected, $actual); } @@ -203,7 +206,7 @@ public function test_get_embed_code_working_with_random_questions(): void { $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); - $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => 'abc123']); + $qbank = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id, 'idnumber' => '']); /** @var \core_question_generator $questiongenerator */ $questiongenerator = $generator->get_plugin_generator('core_question'); @@ -215,7 +218,7 @@ public function test_get_embed_code_working_with_random_questions(): void { $questiongenerator->create_question('shortanswer', null, ['category' => $category->id, 'name' => 'Question2', 'idnumber' => 'frog']); - $embedid = new embed_id('abc123', 'toad'); + $embedid = new embed_id('abc123', 'toad', '*', $course->shortname); $iframedescription = 'Embedded random question'; $behaviour = ''; $maxmark = ''; @@ -232,26 +235,26 @@ public function test_get_embed_code_working_with_random_questions(): void { $token = token::make_secret_token($embedid); $expected = '{Q{' . $embedid . $titlebit . '|' . $token . '}Q}'; - $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, + $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback, $rightanswer, $history, ''); $this->assertEquals($expected, $actual); - $embedid = new embed_id('abc123', 'frog'); + $embedid = new embed_id('abc123', 'frog', '*', $course->shortname); $token = token::make_secret_token($embedid); $expected = '{Q{' . $embedid . $titlebit . '|' . $token . '}Q}'; - $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, + $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback, $rightanswer, $history, ''); $this->assertEquals($expected, $actual); // Accept '*' for $questionidnumber to indicate a random question. - $embedid = new embed_id('abc123', '*'); + $embedid = new embed_id('abc123', '*', '*', $course->shortname); $token = token::make_secret_token($embedid); $expected = '{Q{' . $embedid . $titlebit . '|' . $token . '}Q}'; - $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, + $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback, $rightanswer, $history, ''); @@ -259,7 +262,7 @@ public function test_get_embed_code_working_with_random_questions(): void { $behaviour = 'immediatefeedback'; $expected = '{Q{' . $embedid . $titlebit . '|behaviour=' . $behaviour . '|' . $token . '}Q}'; - $actual = external::get_embed_code($course->id, $embedid->categoryidnumber, + $actual = external::get_embed_code($qbank->cmid, $embedid->categoryidnumber, $embedid->questionidnumber, $iframedescription, $behaviour, $maxmark, $variant, $correctness, $marks, $markdp, $feedback, $generalfeedback, $rightanswer, $history, ''); diff --git a/tests/utils_test.php b/tests/utils_test.php index bf9da24..c80ce96 100644 --- a/tests/utils_test.php +++ b/tests/utils_test.php @@ -421,7 +421,7 @@ public function test_get_shareable_question_banks(): void { $this->assertArrayHasKey($qbank->cmid, $banks); $this->assertArrayHasKey($qbank2->cmid, $banks); - $banks = utils::get_shareable_question_banks($course->id, $course->shortname, null, 'qbank2'); + $banks = utils::get_shareable_question_banks($course->id, null, 'qbank2'); $this->assertArrayHasKey($qbank2->cmid, $banks); $this->assertArrayNotHasKey($qbank->cmid, $banks); } @@ -447,9 +447,7 @@ public function test_get_qbank_by_idnumber(): void { // Check that we can get the question bank. $this->assertEquals($qbank->cmid, utils::get_qbank_by_idnumber($course->id)); - $this->assertEquals($qbank2->cmid, utils::get_qbank_by_idnumber($course->id, '', 'abcd1234')); - // We can get the question bank by with idnumber using course short name. - $this->assertEquals($qbank2->cmid, utils::get_qbank_by_idnumber(SITEID, 'C1', 'abcd1234')); + $this->assertEquals($qbank2->cmid, utils::get_qbank_by_idnumber($course->id, 'abcd1234')); $qbank3 = $this->getDataGenerator()->create_module('qbank', ['course' => $course->id], ['idnumber' => '']); $attemptgenerator->create_embeddable_question('truefalse', null, [], @@ -459,6 +457,6 @@ public function test_get_qbank_by_idnumber(): void { // Can't get a question bank doesn't exist in the course. $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, 'C2')); // Can't get a question bank with an idnumber that does not exist. - $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, '', 'randomidnumber')); + $this->assertEquals(null, utils::get_qbank_by_idnumber($course->id, 'randomidnumber')); } } diff --git a/version.php b/version.php index f656be6..72f1504 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025091600; +$plugin->version = 2025091602; $plugin->requires = 2024042200; // Requires Moodle 4.4. $plugin->component = 'filter_embedquestion'; $plugin->maturity = MATURITY_STABLE;