diff --git a/lib/classes/plugininfo/mod.php b/lib/classes/plugininfo/mod.php index a30493f778a..972f5a3e2fb 100644 --- a/lib/classes/plugininfo/mod.php +++ b/lib/classes/plugininfo/mod.php @@ -163,17 +163,10 @@ class mod extends base { } /** - * Allow all activity modules but Forum to be uninstalled. - * - * This exception for the Forum has been hard-coded in Moodle since ages, - * we may want to re-think it one day. + * Activity modules that declare feature flag FEATURE_CAN_UNINSTALL as false cannot be uninstalled. */ public function is_uninstall_allowed() { - if ($this->name === 'forum') { - return false; - } else { - return true; - } + return plugin_supports('mod', $this->name, FEATURE_CAN_UNINSTALL, true); } /** diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 410e252d382..3cbea0a0ed0 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -451,6 +451,15 @@ define('FEATURE_RATE', 'rate'); /** True if module supports backup/restore of moodle2 format */ define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2'); +/** True if module shares questions with other modules. */ +define('FEATURE_PUBLISHES_QUESTIONS', 'publishesquestions'); + +/** Used to determine if a plugin should render to display */ +define('FEATURE_CAN_DISPLAY', 'candisplay'); + +/** Can this module type be uninstalled */ +define('FEATURE_CAN_UNINSTALL', 'canuninstall'); + /** True if module can show description on course main page */ define('FEATURE_SHOW_DESCRIPTION', 'showdescription'); diff --git a/lib/plugins.json b/lib/plugins.json index a05b2725609..f5f616ffed3 100644 --- a/lib/plugins.json +++ b/lib/plugins.json @@ -325,6 +325,7 @@ "lesson", "lti", "page", + "qbank", "quiz", "resource", "scorm", diff --git a/mod/forum/lib.php b/mod/forum/lib.php index ff9485ef4b4..4781379dde1 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -371,6 +371,8 @@ function forum_supports($feature) { case FEATURE_PLAGIARISM: return true; case FEATURE_ADVANCED_GRADING: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_COLLABORATION; + case FEATURE_CAN_UNINSTALL: + return false; default: return null; } diff --git a/mod/qbank/backup/moodle2/backup_qbank_activity_task.class.php b/mod/qbank/backup/moodle2/backup_qbank_activity_task.class.php new file mode 100644 index 00000000000..abbcbdd5994 --- /dev/null +++ b/mod/qbank/backup/moodle2/backup_qbank_activity_task.class.php @@ -0,0 +1,57 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/qbank/backup/moodle2/backup_qbank_stepslib.php'); + +/** + * Qbank module backup task that provides all the settings and steps to perform one complete backup of the activity. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_qbank_activity_task extends backup_activity_task { + + /** + * Define (add) particular settings this activity can have + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have + */ + protected function define_my_steps() { + // Qbank only has one structure step + $this->add_step(new backup_qbank_activity_structure_step('qbank_structure', 'qbank.xml')); + } + + /** + * Code the transformations to perform in the activity in + * order to get transportable (encoded) links + * + * @param string $content some HTML text that eventually contains URLs to the activity instance scripts + * @return string encoded content + */ + public static function encode_content_links($content) { + return $content; + } +} diff --git a/mod/qbank/backup/moodle2/backup_qbank_stepslib.php b/mod/qbank/backup/moodle2/backup_qbank_stepslib.php new file mode 100644 index 00000000000..26fdce29d28 --- /dev/null +++ b/mod/qbank/backup/moodle2/backup_qbank_stepslib.php @@ -0,0 +1,63 @@ +. + +/** + * Define the complete qbank structure for backup, with file and id annotations. + * + * @package mod_qbank + * @category backup + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class backup_qbank_activity_structure_step extends backup_activity_structure_step { + + /** + * Define the mod_qbank activity structure. + * + * @return backup_nested_element + */ + protected function define_structure(): backup_nested_element { + // Define each element separated. + $qbank = new backup_nested_element( + 'qbank', + ['id'], + [ + 'name', + 'timecreated', + 'timemodified', + 'intro', + 'introformat', + 'type', + ], + ); + + // Build the tree. + // (No tree). + + // Define sources. + $qbank->set_source_table('qbank', ['id' => backup::VAR_ACTIVITYID]); + + // Define id annotations. + // (none). + + // Define file annotations. + $qbank->annotate_files('mod_qbank', 'intro', null); // This file area does not have an itemid. + + // Return the root element (qbank), wrapped into standard activity structure. + return $this->prepare_activity_structure($qbank); + } +} diff --git a/mod/qbank/backup/moodle2/restore_qbank_activity_task.class.php b/mod/qbank/backup/moodle2/restore_qbank_activity_task.class.php new file mode 100644 index 00000000000..62b9a9d4e09 --- /dev/null +++ b/mod/qbank/backup/moodle2/restore_qbank_activity_task.class.php @@ -0,0 +1,76 @@ +. + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/mod/qbank/backup/moodle2/restore_qbank_stepslib.php'); + +/** + * The task that provides a complete restore of mod_qbank. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_qbank_activity_task extends restore_activity_task { + + /** + * Defines particular settings that this activity can have. + */ + protected function define_my_settings(): void { + // No particular settings for this activity. + } + + /** + * Defines particular steps that this activity can have. + * + * @return void. + */ + protected function define_my_steps(): void { + // Qbank only has one structure step. + $this->add_step(new restore_qbank_activity_structure_step('qbank_structure', 'qbank.xml')); + } + + /** + * Defines the contents in the activity that must be processed by the link decoder. + * + * @return array. + */ + public static function define_decode_contents(): array { + return []; + } + + /** + * Defines the decoding rules for links belonging to the activity to be executed by the link decoder. + * + * @return restore_decode_rule[]. + */ + public static function define_decode_rules(): array { + return []; + } + + /** + * Defines the restore log rules that will be applied by the + * {@see restore_logs_processor} when restoring mod_qbank logs. It + * must return one array of {@see restore_log_rule} objects. + * + * @return restore_log_rule[]. + */ + public static function define_restore_log_rules(): array { + return []; + } +} diff --git a/mod/qbank/backup/moodle2/restore_qbank_stepslib.php b/mod/qbank/backup/moodle2/restore_qbank_stepslib.php new file mode 100644 index 00000000000..e03a07f7ea7 --- /dev/null +++ b/mod/qbank/backup/moodle2/restore_qbank_stepslib.php @@ -0,0 +1,63 @@ +. + +/** + * Defines the structure step to restore one mod_qbank activity. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restore_qbank_activity_structure_step extends restore_activity_structure_step { + + /** + * Defines the structure to be restored. + * + * @return restore_path_element[]. + */ + protected function define_structure(): array { + $paths = []; + $paths[] = new restore_path_element('qbank', '/activity/qbank'); + + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * Processes the qbank restore data. + * + * @param array $data Parsed element data. + */ + protected function process_qbank(array $data): void { + global $DB; + + $data['course'] = $this->get_courseid(); + + // Insert the record. + $newitemid = $DB->insert_record('qbank', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * Defines post-execution actions. + */ + protected function after_execute(): void { + // Add qbank related files, no need to match by itemname (just internally handled context). + $this->add_related_files('mod_qbank', 'intro', null); + } +} diff --git a/mod/qbank/classes/event/course_module_instance_list_viewed.php b/mod/qbank/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 00000000000..56bb41971e3 --- /dev/null +++ b/mod/qbank/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,29 @@ +. + +namespace mod_qbank\event; + +/** + * The mod_qbank instance list viewed event class. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { + // No code required here as the parent class handles it all. +} diff --git a/mod/qbank/classes/privacy/provider.php b/mod/qbank/classes/privacy/provider.php new file mode 100644 index 00000000000..e0af8e1bdcb --- /dev/null +++ b/mod/qbank/classes/privacy/provider.php @@ -0,0 +1,37 @@ +. + +namespace mod_qbank\privacy; + +/** + * Privacy Subsystem implementation for mod_qbank. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Return lang string identifier. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/mod/qbank/db/access.php b/mod/qbank/db/access.php new file mode 100644 index 00000000000..38794b227aa --- /dev/null +++ b/mod/qbank/db/access.php @@ -0,0 +1,51 @@ +. + +/** + * Capability definitions for the question bank module. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + // Ability to see that the question bank exists, and the basic information about it. + 'mod/qbank:view' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + ], + // Ability to add a new question bank to the course. + 'mod/qbank:addinstance' => [ + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], +]; diff --git a/mod/qbank/db/install.xml b/mod/qbank/db/install.xml new file mode 100644 index 00000000000..320ce8e33d6 --- /dev/null +++ b/mod/qbank/db/install.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + +
+
+
diff --git a/mod/qbank/db/upgrade.php b/mod/qbank/db/upgrade.php new file mode 100644 index 00000000000..387a27b89d4 --- /dev/null +++ b/mod/qbank/db/upgrade.php @@ -0,0 +1,39 @@ +. + +/** + * Plugin upgrade steps are defined here. + * + * @package mod_qbank + * @category upgrade + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Execute mod_qbank upgrade from the given old version. + * + * @param int $oldversion + * @return bool + */ +function xmldb_qbank_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + return true; +} diff --git a/mod/qbank/index.php b/mod/qbank/index.php new file mode 100644 index 00000000000..a63eb18da3d --- /dev/null +++ b/mod/qbank/index.php @@ -0,0 +1,33 @@ +. + +/** + * This redirect to /question/banks.php to list all the instances of mod_qbank in a particular course. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../../config.php"); + +$id = required_param('id', PARAM_INT); +$course = get_course($id); +require_login($course); + +// Redirect to /question/banks.php as that page shows all the available banks on the course. +redirect(new moodle_url('/question/banks.php', ['courseid' => $course->id])); diff --git a/mod/qbank/lang/en/qbank.php b/mod/qbank/lang/en/qbank.php new file mode 100644 index 00000000000..863dfaf7862 --- /dev/null +++ b/mod/qbank/lang/en/qbank.php @@ -0,0 +1,27 @@ +. + +/** + * Plugin strings are defined here. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Question bank'; +$string['privacy:metadata'] = 'The Question bank plugin does not store any personal data, core_question automatically tracks all sorts of data for questions.'; diff --git a/mod/qbank/lib.php b/mod/qbank/lib.php new file mode 100644 index 00000000000..4e796e8bd99 --- /dev/null +++ b/mod/qbank/lib.php @@ -0,0 +1,109 @@ +. + +/** + * Library of interface functions and constants. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Return if the plugin supports $feature. + * + * @param string $feature Constant representing the feature. + * @return bool|string|null True if module supports feature, false if not, null if it doesn't know or string for the module purpose. + */ +function qbank_supports(string $feature) { + switch ($feature) { + case FEATURE_BACKUP_MOODLE2: + case FEATURE_PUBLISHES_QUESTIONS: + case FEATURE_SHOW_DESCRIPTION: + case FEATURE_USES_QUESTIONS: + return true; + case FEATURE_CAN_DISPLAY: + case FEATURE_CAN_UNINSTALL: + case FEATURE_COMMENT: + case FEATURE_COMPLETION_HAS_RULES: + case FEATURE_COMPLETION_TRACKS_VIEWS: + case FEATURE_CONTROLS_GRADE_VISIBILITY: + case FEATURE_GRADE_OUTCOMES: + case FEATURE_MODEDIT_DEFAULT_COMPLETION: + return false; + case FEATURE_MOD_PURPOSE: + return MOD_PURPOSE_CONTENT; + default: + return null; + } +} + +/** + * Saves a new instance of the mod_qbank into the database. + * + * Given an object containing all the necessary data, + * this function will create a new instance and return the id number of the instance. + * + * @param stdClass $moduleinstance An object from the form. + * @param mod_qbank_mod_form|null $mform The form. Not used in this function. + * @return int The id of the newly inserted record. + */ +function qbank_add_instance(stdClass $moduleinstance, ?mod_qbank_mod_form $mform): int { + global $DB; + + $moduleinstance->timecreated = time(); + + return $DB->insert_record('qbank', $moduleinstance); +} + +/** + * Updates an instance of the mod_qbank in the database. + * Given an object containing all the necessary data, + * this function will update an existing instance with new data. + * + * @param stdClass $moduleinstance An object from the form in mod_form.php. + * @param mod_qbank_mod_form|null $mform The form. Not used in this function. + * @return bool True if successful, false otherwise. + */ +function qbank_update_instance(stdClass $moduleinstance, ?mod_qbank_mod_form $mform): bool { + global $DB; + + $moduleinstance->timemodified = time(); + $moduleinstance->id = $moduleinstance->instance; + + return $DB->update_record('qbank', $moduleinstance); +} + +/** + * Removes an instance of the mod_qbank from the database. + * We don't need to do anything for questions, question_categories, or user records here + * as the module deletion API cleans that up for us. + * + * @param int $id id of the module instance. + * @return bool True if successful, false on failure. + */ +function qbank_delete_instance(int $id): bool { + global $DB; + + if (!$DB->record_exists('qbank', ['id' => $id])) { + return false; + } + + $DB->delete_records('qbank', ['id' => $id]); + + return true; +} diff --git a/mod/qbank/pix/monologo.svg b/mod/qbank/pix/monologo.svg new file mode 100644 index 00000000000..5fefab8e1b4 --- /dev/null +++ b/mod/qbank/pix/monologo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/mod/qbank/tests/backup/restore_date_test.php b/mod/qbank/tests/backup/restore_date_test.php new file mode 100644 index 00000000000..ebe442a7ae5 --- /dev/null +++ b/mod/qbank/tests/backup/restore_date_test.php @@ -0,0 +1,49 @@ +. + +namespace mod_qbank\backup; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); + +/** + * Restore test for mod_qbank. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class restore_date_test extends \restore_date_testcase { + + /** + * When restoring a course, you can change the start date, which shifts other dates. + * This test checks that certain dates are correctly modified. + * + * @covers \restore_dbops::create_new_course() + * @return void + */ + public function test_restore_dates(): void { + global $DB; + + [$course, $module] = $this->create_course_and_module('qbank', ['timemodified' => time()]); + $newcourseid = $this->backup_and_restore($course); + $newmodule = $DB->get_record('qbank', ['course' => $newcourseid]); + $this->assertFieldsNotRolledForward($module, $newmodule, ['timemodified']); + } +} diff --git a/mod/qbank/tests/generator/lib.php b/mod/qbank/tests/generator/lib.php new file mode 100644 index 00000000000..fbffd23a646 --- /dev/null +++ b/mod/qbank/tests/generator/lib.php @@ -0,0 +1,26 @@ +. + +/** + * Generator for mod_qbank. + * Required by the module generator but intentionally blank until we need to extend + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_qbank_generator extends testing_module_generator {} diff --git a/mod/qbank/version.php b/mod/qbank/version.php new file mode 100644 index 00000000000..983c106561d --- /dev/null +++ b/mod/qbank/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @package mod_qbank + * @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Simon Adams + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'mod_qbank'; +$plugin->version = 2024080800; +$plugin->requires = 2024080200;