Файловый менеджер - Редактировать - /home/harasnat/www/mf/local.tar
Назад
readme.txt 0000604 00000030125 15062071257 0006544 0 ustar 00 // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Readme file for local customisations * * @package local * @copyright 2009 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ Local customisations directory ============================== This directory is the recommended place for local customisations. Wherever possible, customisations should be written using one of the standard plug-in points like modules, blocks, auth plugins, themes, etc. See also https://moodledev.io/docs/apis/plugintypes/local for more information. Directory structure ------------------- This directory has standard plugin structure. All standard plugin features are supported. There may be some extra files with special meaning in /local/. Sample /local/ directory listing: /local/nicehack/ - first customisation plugin /local/otherhack/ - other customisation plugin /local/preupgrade.php - executed before each core upgrade, use $version and $CFG->version if you need to tweak specific local hacks /local/defaults.php - custom admin setting defaults Local plugins ============= Local plugins are used in cases when no standard plugin fits, examples are: * event consumers communicating with external systems * custom definitions of web services and external functions * applications that extend moodle at the system level (hub server, amos server, etc.) * new database tables used in core hacks (discouraged) * new capability definitions used in core hacks * custom admin settings Standard plugin features: * /local/pluginname/version.php - version of script (must be incremented after changes) * /local/pluginname/db/install.xml - executed during install (new version.php found) * /local/pluginname/db/install.php - executed right after install.xml * /local/pluginname/db/uninstall.php - executed during uninstallation * /local/pluginname/db/upgrade.php - executed after version.php change * /local/pluginname/db/access.php - definition of capabilities * /local/pluginname/db/events.php - event handlers and subscripts * /local/pluginname/db/messages.php - messaging registration * /local/pluginname/db/services.php - definition of web services and web service functions * /local/pluginname/db/subplugins.php - list of subplugins types supported by this local plugin * /local/pluginname/lang/en/local_pluginname.php - language file * /local/pluginname/settings.php - admin settings Local plugin version specification ---------------------------------- version.php is mandatory for most of the standard plugin infrastructure. The version number must be incremented most plugin changes, the changed version tells Moodle to invalidate all caches, do db upgrades if necessary, install new capabilities, register event handlers, etc. Example: /local/nicehack/version.php <?php $plugin->version = 2010022400; // The (date) version of this plugin $plugin->requires = 2010021900; // Requires this Moodle version Local plugin capabilities ------------------------- Each local plugin may define own capabilities. It is not recommended to define capabilities belonging to other plugins here, but it should work too. /local/nicehack/access.php content <?php $local_nicehack_capabilities = array( 'local/nicehack:nicecapability' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_SYSTEM, ), ); Local plugin language strings ----------------------------- If customisation needs new strings it is recommended to use normal plugin strings. sample language file /local/nicehack/lang/en/local_nicehack.php <?php $string['hello'] = 'Hi {$a}'; $string['nicehack:nicecapability'] = 'Some capability'; use of the new string in code: echo get_string('hello', 'local_nicehack', 'petr'); Local plugin admin menu items ----------------------------- It is possible to add new items and categories to the admin_tree block. I you need to define new admin setting classes put them into separate file and require_once() from settings.php For example if you want to add new external page use following /local/nicehack/settings.php <?php $ADMIN->add('root', new admin_category('tweaks', 'Custom tweaks')); $ADMIN->add('tweaks', new admin_externalpage('nicehackery', 'Tweak something', $CFG->wwwroot.'/local/nicehack/setuppage.php')); Or if you want a new standard settings page for the plugin, inside the local plugins category: <?php defined('MOODLE_INTERNAL') || die; if ($hassiteconfig) { // needs this condition or there is error on login page $settings = new admin_settingpage('local_thisplugin', 'This plugin'); $ADMIN->add('localplugins', $settings); $settings->add(new admin_setting_configtext('local_thisplugin/option', 'Option', 'Information about this option', 100, PARAM_INT)); } Local plugin event handlers --------------------------- Events are intended primarily for communication "core --> plugins". (It should not be use in opposite direction!) In theory it could be also used for "plugin --> plugin" communication too. The list of core events is documented in lib/db/events.php sample files /local/nicehack/db/events.php $handlers = array ( 'user_deleted' => array ( 'handlerfile' => '/local/nicehack/lib.php', 'handlerfunction' => 'nicehack_userdeleted_handler', 'schedule' => 'instant' ), ); NOTE: events are not yet fully implemented in current Moodle 2.0dev. Local plugin database tables ---------------------------- XMLDB editors is the recommended tool. Please note that modification of core table structure is highly discouraged. If you really really really need to modify core tables you might want to do that in install.php and later upgrade.php Note: it is forbidden to manually modify the DB structure, without corresponding changes in install.xml files. List of upgrade related files: /local/nicehack/db/install.xml - contains XML definition of new tables /local/nicehack/db/install.php - executed after db creation, may be also used for general install code /local/nicehack/db/upgrade.php - executed when version changes Local plugin web services ------------------------- During plugin installation or upgrade, the web service definitions are read from /local/nicehack/db/services.php and are automatically installed/updated in Moodle. sample files /local/nicehack/db/services.php $$functions = array ( 'nicehack_hello_world' => array( 'classname' => 'local_nicehack_external', 'methodname' => 'hello_world', 'classpath' => 'local/nicehack/externallib.php', 'description' => 'Get hello world string', 'type' => 'read', ), ); $services = array( 'Nice hack service 1' => array( 'functions' => array ('nicehack_hello_world'), 'enabled'=>1, ), ); You will need to write the /local/nicehack/externallib.php - external functions description and code. See some examples from the core files (/user/externallib.php, /group/externallib.php...). Local plugin navigation hooks ----------------------------- There are two functions that your plugin can define that allow it to extend the main navigation and the settings navigation. These two functions both need to be defined within /local/nicehack/lib.php. sample code <?php function local_nicehack_extend_navigation(global_navigation $nav) { // $nav is the global navigation instance. // Here you can add to and manipulate the navigation structure as you like. // This callback was introduced in 2.0 as nicehack_extends_navigation(global_navigation $nav) // In 2.3 support was added for local_nicehack_extends_navigation(global_navigation $nav). // In 2.9 the name was corrected to local_nicehack_extend_navigation() for consistency } function local_nicehack_extend_settings_navigation(settings_navigation $nav, context $context) { // $nav is the settings navigation instance. // $context is the context the settings have been loaded for (settings is context specific) // Here you can add to and manipulate the settings structure as you like. // This callback was introduced in 2.3, originally as local_nicehack_extends_settings_navigation() // In 2.9 the name was corrected to the imperative mood ('extend', not 'extends') } Other local customisation files =============================== Customised site defaults ------------------------ Different default site settings can be stored in file /local/defaults.php. These new defaults are used during installation, upgrade and later are displayed as default values in admin settings. This means that the content of the defaults files is usually updated BEFORE installation or upgrade. These customised defaults are useful especially when using CLI tools for installation and upgrade. Sample /local/defaults.php file content: <?php $defaults['moodle']['forcelogin'] = 1; // new default for $CFG->forcelogin $defaults['scorm']['maxgrade'] = 20; // default for get_config('scorm', 'maxgrade') $defaults['moodlecourse']['numsections'] = 11; $defaults['moodle']['hiddenuserfields'] = array('city', 'country'); First bracket contains string from column plugin of config_plugins table. Second bracket is the name of setting. In the admin settings UI the plugin and name of setting is separated by "|". The values usually correspond to the raw string in config table, with the exception of comma separated lists that are usually entered as real arrays. Please note that not all settings are converted to admin_tree, they are mostly intended to be set directly in config.php. 2.0 pre-upgrade script ---------------------- You an use /local/upgrade_pre20.php script for any code that needs to be executed before the main upgrade to 2.0. Most probably this will be used for undoing of old hacks that would otherwise break normal 2.0 upgrade. This file is just included directly, there does not need to be any function inside. If the execution stops the script is executed again during the next upgrade. The first execution of lib/db/upgrade.php increments the version number and the pre upgrade script is not executed any more. 1.9.x upgrade notes =================== 1.9.x contains basic support for local hacks placed directly into /local/ directory. This old local API was completely removed and can not be used any more in 2.0. All old customisations need to be migrated to new local plugins before running of the 2.0 upgrade script. Other site customisation outside of "/local/" directory ======================================================= Local language pack modifications --------------------------------- Moodle supports other type of local customisation of standard language packs. If you want to create your own language pack based on another language create new dataroot directory with "_local" suffix, for example following file with content changes string "Login" to "Sign in": moodledata/lang/en_local <?php $string['login'] = 'Sign in'; See also http://docs.moodle.org/en/Language_editing Custom script injection ----------------------- Very old customisation option that allows you to modify scripts by injecting code right after the require 'config.php' call. This setting is enabled by manually setting $CFG->customscripts variable in config.php script. The value is expected to be full path to directory with the same structure as dirroot. Please note this hack only affects files that actually include the config.php! Examples: * disable one specific moodle page without code modification * alter page parameters on the fly upgrade.txt 0000604 00000001446 15062071257 0006742 0 ustar 00 This file describes API changes for the plugins of the type 'local'. === 3.1 === * Navigation API callbacks local_<plugin>_extends_navigation() and local_<plugin>_extends_settings_navigation() have been removed. Please rename them to local_<plugin>_extend_navigation() and local_<plugin>_extend_settings_navigation() respectively. === 2.9 === * Navigation API callbacks local_<plugin>_extends_navigation() and local_<plugin>_extends_settings_navigation() are deprecated. Please rename them to local_<plugin>_extend_navigation() and local_<plugin>_extend_settings_navigation() respectively. The deprecated variant will be supported in 2.9 and 3.0 and then the support will be dropped. * Definitely dropped support for the original <plugin>_extends_navigation() that has been deprecated since 2.3. lang/en/repository_local.php 0000604 00000002726 15062104520 0012167 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Strings for component 'repository_local', language 'en', branch 'MOODLE_20_STABLE' * * @package repository_local * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configplugin'] = 'Configuration for server file repository'; $string['currentusefiles'] = 'Currently used files'; $string['emptyfilelist'] = 'There are no files to show'; $string['local:view'] = 'View server repository'; $string['notitle'] = 'notitle'; $string['remember'] = 'Remember me'; $string['pluginname_help'] = 'Files previously uploaded to the Moodle server'; $string['pluginname'] = 'Server files'; $string['privacy:metadata'] = 'The Server files repository plugin does not store or transmit any personal data.'; classes/privacy/provider.php 0000604 00000002761 15062104520 0012216 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for repository_local. * * @package repository_local * @copyright 2018 Zig Tan <zig@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace repository_local\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for repository_local implementing null_provider. * * @copyright 2018 Zig Tan <zig@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } lib.php 0000604 00000033243 15062104520 0006017 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This plugin is used to access local files * * @since Moodle 2.0 * @package repository_local * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot . '/repository/lib.php'); /** * repository_local class is used to browse moodle files * * @since Moodle 2.0 * @package repository_local * @copyright 2012 Marina Glancy * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_local extends repository { /** * Get file listing * * @param string $encodedpath * @param string $page no paging is used in repository_local * @return mixed */ public function get_listing($encodedpath = '', $page = '') { global $CFG, $USER, $OUTPUT; $ret = array(); $ret['dynload'] = true; $ret['nosearch'] = false; $ret['nologin'] = true; $ret['list'] = array(); $itemid = null; $filename = null; $filearea = null; $filepath = null; $component = null; if (!empty($encodedpath)) { $params = json_decode(base64_decode($encodedpath), true); if (is_array($params) && isset($params['contextid'])) { $component = is_null($params['component']) ? NULL : clean_param($params['component'], PARAM_COMPONENT); $filearea = is_null($params['filearea']) ? NULL : clean_param($params['filearea'], PARAM_AREA); $itemid = is_null($params['itemid']) ? NULL : clean_param($params['itemid'], PARAM_INT); $filepath = is_null($params['filepath']) ? NULL : clean_param($params['filepath'], PARAM_PATH); $filename = is_null($params['filename']) ? NULL : clean_param($params['filename'], PARAM_FILE); $context = context::instance_by_id(clean_param($params['contextid'], PARAM_INT)); } } if (empty($context) && !empty($this->context)) { $context = $this->context->get_course_context(false); } if (empty($context)) { $context = context_system::instance(); } // prepare list of allowed extensions: $extensions is either string '*' // or array of lowercase extensions, i.e. array('.gif','.jpg') $extensions = optional_param_array('accepted_types', '', PARAM_RAW); if (empty($extensions) || $extensions === '*' || (is_array($extensions) && in_array('*', $extensions))) { $extensions = '*'; } else { if (!is_array($extensions)) { $extensions = array($extensions); } $extensions = array_map('core_text::strtolower', $extensions); } // build file tree $browser = get_file_browser(); if (!($fileinfo = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename))) { // if file doesn't exist, build path nodes root of current context $fileinfo = $browser->get_file_info($context, null, null, null, null, null); } $ret['list'] = $this->get_non_empty_children($fileinfo, $extensions); // build path navigation $path = array(); for ($level = $fileinfo; $level; $level = $level->get_parent()) { array_unshift($path, $level); } array_unshift($path, null); $ret['path'] = array(); for ($i=1; $i<count($path); $i++) { if ($path[$i] == $fileinfo || !$this->can_skip($path[$i], $extensions, $path[$i-1])) { $ret['path'][] = $this->get_node_path($path[$i]); } } return $ret; } /** * Tells how the file can be picked from this repository * * @return int */ public function supported_returntypes() { return FILE_INTERNAL | FILE_REFERENCE; } /** * Does this repository used to browse moodle files? * * @return boolean */ public function has_moodle_files() { return true; } /** * Returns all children elements that have one of the specified extensions * * This function may skip subfolders and recursively add their children * {@link repository_local::can_skip()} * * @param file_info $fileinfo * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @return array array of file_info elements */ private function get_non_empty_children(file_info $fileinfo, $extensions) { $nonemptychildren = $fileinfo->get_non_empty_children($extensions); $list = array(); foreach ($nonemptychildren as $child) { if ($this->can_skip($child, $extensions, $fileinfo)) { $list = array_merge($list, $this->get_non_empty_children($child, $extensions)); } else { $list[] = $this->get_node($child); } } return $list; } /** * Whether this folder may be skipped in folder hierarchy * * 1. Skip the name of a single filearea in a module * 2. Skip course categories for non-admins who do not have navshowmycoursecategories setting * * @param file_info $fileinfo * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param file_info|int $parent specify parent here if we know it to avoid creating extra objects * @return bool */ private function can_skip(file_info $fileinfo, $extensions, $parent = -1) { global $CFG; if (!$fileinfo->is_directory()) { // do not skip files return false; } if ($fileinfo instanceof file_info_context_course || $fileinfo instanceof file_info_context_user || $fileinfo instanceof file_info_area_course_legacy || $fileinfo instanceof file_info_context_module || $fileinfo instanceof file_info_context_system) { // These instances can never be filearea inside an activity, they will never be skipped. return false; } else if ($fileinfo instanceof file_info_context_coursecat) { // This is a course category. For non-admins we do not display categories return empty($CFG->navshowmycoursecategories) && !has_capability('moodle/course:update', context_system::instance()); } else { $params = $fileinfo->get_params(); if (strlen($params['filearea']) && ($params['filepath'] === '/' || empty($params['filepath'])) && ($params['filename'] === '.' || empty($params['filename'])) && context::instance_by_id($params['contextid'])->contextlevel == CONTEXT_MODULE) { if ($parent === -1) { $parent = $fileinfo->get_parent(); } // This is a filearea inside an activity, it can be skipped if it has no non-empty siblings if ($parent && ($parent instanceof file_info_context_module)) { if ($parent->count_non_empty_children($extensions, 2) <= 1) { return true; } } } } return false; } /** * Converts file_info object to element of repository return list * * @param file_info $fileinfo * @return array */ private function get_node(file_info $fileinfo) { global $OUTPUT; $encodedpath = base64_encode(json_encode($fileinfo->get_params())); $node = array( 'title' => $fileinfo->get_visible_name(), 'datemodified' => $fileinfo->get_timemodified(), 'datecreated' => $fileinfo->get_timecreated() ); if ($fileinfo->is_directory()) { $node['path'] = $encodedpath; $node['thumbnail'] = $OUTPUT->image_url(file_folder_icon())->out(false); $node['children'] = array(); } else { $node['size'] = $fileinfo->get_filesize(); $node['author'] = $fileinfo->get_author(); $node['license'] = $fileinfo->get_license(); $node['isref'] = $fileinfo->is_external_file(); if ($fileinfo->get_status() == 666) { $node['originalmissing'] = true; } $node['source'] = $encodedpath; $node['thumbnail'] = $OUTPUT->image_url(file_file_icon($fileinfo))->out(false); $node['icon'] = $OUTPUT->image_url(file_file_icon($fileinfo))->out(false); if ($imageinfo = $fileinfo->get_imageinfo()) { // what a beautiful picture, isn't it $fileurl = new moodle_url($fileinfo->get_url()); $node['realthumbnail'] = $fileurl->out(false, array('preview' => 'thumb', 'oid' => $fileinfo->get_timemodified())); $node['realicon'] = $fileurl->out(false, array('preview' => 'tinyicon', 'oid' => $fileinfo->get_timemodified())); $node['image_width'] = $imageinfo['width']; $node['image_height'] = $imageinfo['height']; } } return $node; } /** * Converts file_info object to element of repository return path * * @param file_info $fileinfo * @return array */ private function get_node_path(file_info $fileinfo) { $encodedpath = base64_encode(json_encode($fileinfo->get_params())); return array( 'path' => $encodedpath, 'name' => $fileinfo->get_visible_name() ); } /** * Search through all the files. * * This method will do a raw search through the database, then will try * to match with files that a user can access. A maximum of 50 files will be * returned at a time, excluding possible duplicates found along the way. * * Queries are done in chunk of 100 files to prevent too many records to be fetched * at once. When too many files are not included, or a maximum of 10 queries are * performed we consider that this was the last page. * * @param String $q The query string. * @param integer $page The page number. * @return array of results. */ public function search($q, $page = 1) { global $DB, $SESSION; // Because the repository API is weird, the first page is 0, but it should be 1. if (!$page) { $page = 1; } if (!isset($SESSION->repository_local_search)) { $SESSION->repository_local_search = array(); } $fs = get_file_storage(); $fb = get_file_browser(); $max = 50; $limit = 100; if ($page <= 1) { $SESSION->repository_local_search['query'] = $q; $SESSION->repository_local_search['from'] = 0; $from = 0; } else { // Yes, the repository does not send the query again... $q = $SESSION->repository_local_search['query']; $from = (int) $SESSION->repository_local_search['from']; } $count = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', null, null, true); $remaining = $count - $from; $maxloops = 3000; $loops = 0; $results = array(); while (count($results) < $max && $maxloops > 0 && $remaining > 0) { if (empty($files)) { $files = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', $from, $limit); $from += $limit; }; $remaining--; $maxloops--; $loops++; $file = array_shift($files); if (!$file) { // This should not happen. throw new coding_exception('Unexpected end of files list.'); } $key = $file->get_contenthash() . ':' . $file->get_filename(); if (isset($results[$key])) { // We found the file with same content and same name, let's skip it. continue; } $ctx = context::instance_by_id($file->get_contextid()); $fileinfo = $fb->get_file_info($ctx, $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename()); if ($fileinfo) { $results[$key] = $this->get_node($fileinfo); } } // Save the position for the paging to work. if ($maxloops > 0 && $remaining > 0) { $SESSION->repository_local_search['from'] += $loops; $pages = -1; } else { $SESSION->repository_local_search['from'] = 0; $pages = 0; } $return = array( 'list' => array_values($results), 'dynload' => true, 'pages' => $pages, 'page' => $page ); return $return; } /** * Is this repository accessing private data? * * @return bool */ public function contains_private_data() { return false; } } db/install.php 0000604 00000001736 15062104520 0007306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. function xmldb_repository_local_install() { global $CFG; $result = true; require_once($CFG->dirroot.'/repository/lib.php'); $local_plugin = new repository_type('local', array(), true); if(!$id = $local_plugin->create(true)) { $result = false; } return $result; } db/access.php 0000604 00000002415 15062104520 0007074 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Plugin capabilities. * * @package repository_local * @copyright 2009 Dongsheng Cai * @author Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'repository/local:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'coursecreator' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); version.php 0000604 00000002302 15062104520 0006726 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Version details * * @package repository * @subpackage local * @copyright 2009 Dongsheng Cai * @author Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2023100900; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2023100400; // Requires this Moodle version. $plugin->component = 'repository_local'; // Full name of the plugin (used for diagnostics) tests/generator/lib.php 0000604 00000002245 15062104520 0011145 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Local repository data generator * * @package repository_local * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Local repository data generator class * * @package repository_local * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class repository_local_generator extends testing_repository_generator { } pix/icon.png 0000604 00000000655 15062104520 0006777 0 ustar 00 �PNG IHDR �a tIDATx��3�Q��mo۶U'Ultqb�L۶m��083��֯y���y�C�7E��4����Yj[V��ļ���Շ��� \��ѣ� ��/=16՟ ���4����%|�̘575��t���k�P-�?cY&���Ln�ҢN�����'P�7�� 9�`����5�p(ąs�['$&���ׯ��ԉ��G����"~�~*�DP�4y�� �Z�q� �DU�s��-R�"�M1� ��A +ED��ޯ'��~B5�W͖&)i�mXY�y6�>����1g�ƍ>�8�k%�@`*@�T�I���[�k7J�� �@>�H�Y���� ���+�<�M��ߞ��`��0� IEND�B`� pix/icon.svg 0000604 00000011650 15062104520 0007007 0 ustar 00 <?xml version="1.0" encoding="utf-8"?> <!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/"> ]> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" x="0px" y="0px" width="74px" height="51px" viewBox="-0.535 -0.774 74 51" style="overflow:visible;enable-background:new -0.535 -0.774 74 51;" xml:space="preserve" preserveAspectRatio="xMinYMid meet"> <defs> </defs> <radialGradient id="SVGID_1_" cx="137.084" cy="32.2324" r="123.3346" gradientUnits="userSpaceOnUse"> <stop offset="0" style="stop-color:#FAAF40"/> <stop offset="0.0432" style="stop-color:#F9A538"/> <stop offset="0.1116" style="stop-color:#F89D31"/> <stop offset="0.2269" style="stop-color:#F89A2F"/> <stop offset="0.5276" style="stop-color:#F7922D"/> <stop offset="1" style="stop-color:#F37B28"/> <a:midPointStop offset="0" style="stop-color:#FAAF40"/> <a:midPointStop offset="0.1982" style="stop-color:#FAAF40"/> <a:midPointStop offset="0.2269" style="stop-color:#F89A2F"/> <a:midPointStop offset="0.6064" style="stop-color:#F89A2F"/> <a:midPointStop offset="1" style="stop-color:#F37B28"/> </radialGradient> <path style="fill:url(#SVGID_1_);" d="M61.192,49.375V26.558c0-4.771-1.972-7.156-5.911-7.156c-3.942,0-5.912,2.386-5.912,7.156 v22.817H37.754V26.558c0-4.771-1.938-7.156-5.811-7.156c-3.94,0-5.91,2.386-5.91,7.156v22.817H14.417V25.211 c0-4.979,1.728-8.747,5.185-11.304c3.043-2.282,7.158-3.425,12.343-3.425c5.255,0,9.127,1.349,11.617,4.045 c2.142-2.696,6.049-4.045,11.72-4.045c5.186,0,9.298,1.143,12.341,3.425c3.457,2.557,5.186,6.325,5.186,11.304v24.164H61.192z"/> <path style="fill:#58595B;" d="M15.499,16.321c-0.394,1.999-0.788,3.999-1.181,5.997c10.983,3.72,21.4,0.111,26.883-9.489 C33.184,7.282,25.601,12.914,15.499,16.321"/> <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="14.4287" y1="16.0166" x2="40.6895" y2="16.0166"> <stop offset="0" style="stop-color:#929497"/> <stop offset="0.1245" style="stop-color:#757578"/> <stop offset="0.2792" style="stop-color:#575658"/> <stop offset="0.4403" style="stop-color:#403E3F"/> <stop offset="0.6085" style="stop-color:#302D2E"/> <stop offset="0.7884" style="stop-color:#262223"/> <stop offset="1" style="stop-color:#231F20"/> <a:midPointStop offset="0" style="stop-color:#929497"/> <a:midPointStop offset="0.2606" style="stop-color:#929497"/> <a:midPointStop offset="1" style="stop-color:#231F20"/> </linearGradient> <path style="fill:url(#SVGID_2_);" d="M15.499,14.889c-0.356,2.259-0.714,4.518-1.07,6.776c10.527,3.567,20.84,0.503,26.261-8.944 C33.708,4.677,25.925,11.373,15.499,14.889"/> <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="19.457" y1="7.248" x2="30.3611" y2="22.8207"> <stop offset="0" style="stop-color:#231F20"/> <stop offset="1" style="stop-color:#231F20;stop-opacity:0"/> <a:midPointStop offset="0" style="stop-color:#231F20"/> <a:midPointStop offset="0.5" style="stop-color:#231F20"/> <a:midPointStop offset="1" style="stop-color:#231F20;stop-opacity:0"/> </linearGradient> <path style="fill:url(#SVGID_3_);" d="M27.993,17.575c-4.764-0.997-10.036,1.485-13.564,4.09 C12.103,4.879,22.536,5.222,36.098,9.415c-0.857,4.143-2.387,9.598-5.017,12.903C31.081,20.182,30.052,18.6,27.993,17.575"/> <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="0.0078" y1="7.7275" x2="48.0068" y2="7.7275"> <stop offset="0" style="stop-color:#929497"/> <stop offset="0.1245" style="stop-color:#757578"/> <stop offset="0.2792" style="stop-color:#575658"/> <stop offset="0.4403" style="stop-color:#403E3F"/> <stop offset="0.6085" style="stop-color:#302D2E"/> <stop offset="0.7884" style="stop-color:#262223"/> <stop offset="1" style="stop-color:#231F20"/> <a:midPointStop offset="0" style="stop-color:#929497"/> <a:midPointStop offset="0.2606" style="stop-color:#929497"/> <a:midPointStop offset="1" style="stop-color:#231F20"/> </linearGradient> <path style="fill:url(#SVGID_4_);" d="M0.008,14.889C17.625,4.318,27.468,2.282,48.007,0.202 C24.279,18.919,23.64,14.889,0.008,14.889"/> <line style="fill:#383738;stroke:#4A4A4C;stroke-width:0.5;" x1="48.007" y1="0.202" x2="29.186" y2="13.948"/> <path style="opacity:0.23;fill:#231F20;" d="M25.506,7.567C25.733,9.723,25.286,5.423,25.506,7.567"/> <line style="fill:#FFFFFF;stroke:#A8ABAD;stroke-width:0.5;" x1="0.008" y1="14.889" x2="29.186" y2="13.948"/> <path style="fill:none;stroke:#F16922;stroke-width:0.5;" d="M23.79,7.733c-4.996,1.381-21.387,5.041-21.64,7.086 c-0.483,3.917-0.123,10.143-0.123,10.143"/> <path style="fill:#F16922;" d="M3.532,39.84C1.697,35.389-0.443,30.363,2.1,24.184C3.794,29.957,3.544,34.362,3.532,39.84"/> <ellipse transform="matrix(0.942 -0.3356 0.3356 0.942 -1.1544 8.1596)" style="fill:#6D6E70;" cx="23.032" cy="7.42" rx="0.792" ry="0.411"/> </svg> local.xml 0000644 00000003507 15062121201 0006353 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <extension type="plugin" group="filesystem" method="upgrade"> <name>plg_filesystem_local</name> <author>Joomla! Project</author> <creationDate>2017-04</creationDate> <copyright>(C) 2017 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>4.0.0</version> <description>PLG_FILESYSTEM_LOCAL_XML_DESCRIPTION</description> <namespace path="src">Joomla\Plugin\Filesystem\Local</namespace> <files> <folder plugin="local">services</folder> <folder>src</folder> </files> <languages> <language tag="en-GB">language/en-GB/plg_filesystem_local.ini</language> <language tag="en-GB">language/en-GB/plg_filesystem_local.sys.ini</language> </languages> <config> <fields name="params"> <fieldset name="basic"> <field name="directories" type="subform" label="PLG_FILESYSTEM_LOCAL_DIRECTORIES_LABEL" multiple="true" layout="joomla.form.field.subform.repeatable-table" buttons="add,remove,move" default='[{"directory":"images"}]' > <form> <field name="directory" type="folderlist" default="images" label="PLG_FILESYSTEM_LOCAL_DIRECTORIES_DIRECTORY_LABEL" folderFilter="" exclude="" stripext="" hide_none="true" validate="options" /> <field name="thumbs" type="radio" label="PLG_FILESYSTEM_LOCAL_DIRECTORIES_DIRECTORY_THUMBNAILS_LABEL" layout="joomla.form.field.radio.switcher" default="0" filter="integer" > <option value="0">JNO</option> <option value="1">JYES</option> </field> </form> </field> </fieldset> </fields> </config> </extension> services/provider.php 0000644 00000002443 15062121201 0010723 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage Filesystem.local * * @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Filesystem\Local\Extension\Local; return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.3.0 */ public function register(Container $container) { $container->set( PluginInterface::class, function (Container $container) { $plugin = new Local( $container->get(DispatcherInterface::class), (array) PluginHelper::getPlugin('filesystem', 'local'), JPATH_ROOT ); $plugin->setLanguage(Factory::getApplication()->getLanguage()); return $plugin; } ); } }; src/Extension/Local.php 0000644 00000007241 15062121201 0011044 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage FileSystem.local * * @copyright (C) 2017 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Filesystem\Local\Extension; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Component\Media\Administrator\Event\MediaProviderEvent; use Joomla\Component\Media\Administrator\Provider\ProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Filesystem\Local\Adapter\LocalAdapter; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * FileSystem Local plugin. * * The plugin to deal with the local filesystem in Media Manager. * * @since 4.0.0 */ final class Local extends CMSPlugin implements ProviderInterface { /** * Affects constructor behavior. If true, language files will be loaded automatically. * * @var boolean * @since 4.0.0 */ protected $autoloadLanguage = true; /** * The root directory path * * @var string * @since 4.3.0 */ private $rootDirectory; /** * Constructor. * * @param DispatcherInterface $dispatcher The dispatcher * @param array $config An optional associative array of configuration settings * @param string $rootDirectory The root directory to look for images * * @since 4.3.0 */ public function __construct(DispatcherInterface $dispatcher, array $config, string $rootDirectory) { parent::__construct($dispatcher, $config); $this->rootDirectory = $rootDirectory; } /** * Setup Providers for Local Adapter * * @param MediaProviderEvent $event Event for ProviderManager * * @return void * * @since 4.0.0 */ public function onSetupProviders(MediaProviderEvent $event) { $event->getProviderManager()->registerProvider($this); } /** * Returns the ID of the provider * * @return string * * @since 4.0.0 */ public function getID() { return $this->_name; } /** * Returns the display name of the provider * * @return string * * @since 4.0.0 */ public function getDisplayName() { return $this->getLanguage()->_('PLG_FILESYSTEM_LOCAL_DEFAULT_NAME'); } /** * Returns and array of adapters * * @return \Joomla\Component\Media\Administrator\Adapter\AdapterInterface[] * * @since 4.0.0 */ public function getAdapters() { $adapters = []; $directories = $this->params->get('directories', '[{"directory": "images", "thumbs": 0}]'); // Do a check if default settings are not saved by user, if not initialize them manually if (is_string($directories)) { $directories = json_decode($directories); } foreach ($directories as $directoryEntity) { if (!$directoryEntity->directory) { continue; } $directoryPath = $this->rootDirectory . '/' . $directoryEntity->directory; $directoryPath = rtrim($directoryPath) . '/'; if (!isset($directoryEntity->thumbs)) { $directoryEntity->thumbs = 0; } $adapter = new LocalAdapter( $directoryPath, $directoryEntity->directory, $directoryEntity->thumbs, [200, 200] ); $adapters[$adapter->getAdapterName()] = $adapter; } return $adapters; } } src/Adapter/LocalAdapter.php 0000644 00000072060 15062121201 0011752 0 ustar 00 <?php /** * @package Joomla.Plugin * @subpackage Filesystem.local * * @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Filesystem\Local\Adapter; use Joomla\CMS\Date\Date; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Helper\MediaHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Image\Exception\UnparsableImageException; use Joomla\CMS\Image\Image; use Joomla\CMS\Language\Text; use Joomla\CMS\String\PunycodeHelper; use Joomla\CMS\Uri\Uri; use Joomla\Component\Media\Administrator\Adapter\AdapterInterface; use Joomla\Component\Media\Administrator\Exception\FileNotFoundException; use Joomla\Component\Media\Administrator\Exception\InvalidPathException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Local file adapter. * * @since 4.0.0 */ class LocalAdapter implements AdapterInterface { /** * The root path to gather file information from. * * @var string * * @since 4.0.0 */ private $rootPath = null; /** * The file_path of media directory related to site * * @var string * * @since 4.0.0 */ private $filePath = null; /** * Should the adapter create a thumbnail for the image? * * @var boolean * * @since 4.3.0 */ private $thumbnails = false; /** * Thumbnail dimensions in pixels, [0] = width, [1] = height * * @var array * * @since 4.3.0 */ private $thumbnailSize = [200, 200]; /** * The absolute root path in the local file system. * * @param string $rootPath The root path * @param string $filePath The file path of media folder * @param boolean $thumbnails The thumbnails option * @param array $thumbnailSize The thumbnail dimensions in pixels * * @since 4.0.0 */ public function __construct(string $rootPath, string $filePath, bool $thumbnails = false, array $thumbnailSize = [200, 200]) { if (!file_exists($rootPath)) { throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_MISSING_DIR')); } $this->rootPath = Path::clean(realpath($rootPath), '/'); $this->filePath = $filePath; $this->thumbnails = $thumbnails; $this->thumbnailSize = $thumbnailSize; if ($this->thumbnails) { $dir = JPATH_ROOT . '/media/cache/com_media/thumbs/' . $this->filePath; if (!is_dir($dir)) { Folder::create($dir); } } } /** * Returns the requested file or folder. The returned object * has the following properties available: * - type: The type can be file or dir * - name: The name of the file * - path: The relative path to the root * - extension: The file extension * - size: The size of the file * - create_date: The date created * - modified_date: The date modified * - mime_type: The mime type * - width: The width, when available * - height: The height, when available * * If the path doesn't exist a FileNotFoundException is thrown. * * @param string $path The path to the file or folder * * @return \stdClass * * @since 4.0.0 * @throws \Exception */ public function getFile(string $path = '/'): \stdClass { // Get the local path $basePath = $this->getLocalPath($path); // Check if file exists if (!file_exists($basePath)) { throw new FileNotFoundException(); } return $this->getPathInformation($basePath); } /** * Returns the folders and files for the given path. The returned objects * have the following properties available: * - type: The type can be file or dir * - name: The name of the file * - path: The relative path to the root * - extension: The file extension * - size: The size of the file * - create_date: The date created * - modified_date: The date modified * - mime_type: The mime type * - width: The width, when available * - height: The height, when available * * If the path doesn't exist a FileNotFoundException is thrown. * * @param string $path The folder * * @return \stdClass[] * * @since 4.0.0 * @throws \Exception */ public function getFiles(string $path = '/'): array { // Get the local path $basePath = $this->getLocalPath($path); // Check if file exists if (!file_exists($basePath)) { throw new FileNotFoundException(); } // Check if the path points to a file if (is_file($basePath)) { return [$this->getPathInformation($basePath)]; } // The data to return $data = []; // Read the folders foreach (Folder::folders($basePath) as $folder) { $data[] = $this->getPathInformation(Path::clean($basePath . '/' . $folder)); } // Read the files foreach (Folder::files($basePath) as $file) { $data[] = $this->getPathInformation(Path::clean($basePath . '/' . $file)); } // Return the data return $data; } /** * Returns a resource to download the path. * * @param string $path The path to download * * @return resource * * @since 4.0.0 * @throws \Exception */ public function getResource(string $path) { return fopen($this->rootPath . '/' . $path, 'r'); } /** * Creates a folder with the given name in the given path. * * It returns the new folder name. This allows the implementation * classes to normalise the file name. * * @param string $name The name * @param string $path The folder * * @return string * * @since 4.0.0 * @throws \Exception */ public function createFolder(string $name, string $path): string { $name = $this->getSafeName($name); $localPath = $this->getLocalPath($path . '/' . $name); Folder::create($localPath); return $name; } /** * Creates a file with the given name in the given path with the data. * * It returns the new file name. This allows the implementation * classes to normalise the file name. * * @param string $name The name * @param string $path The folder * @param string $data The data * * @return string * * @since 4.0.0 * @throws \Exception */ public function createFile(string $name, string $path, $data): string { $name = $this->getSafeName($name); $localPath = $this->getLocalPath($path . '/' . $name); $this->checkContent($localPath, $data); File::write($localPath, $data); if ($this->thumbnails && MediaHelper::isImage(pathinfo($localPath)['basename'])) { $thumbnailPaths = $this->getLocalThumbnailPaths($localPath); if (empty($thumbnailPaths)) { return $name; } // Create the thumbnail $this->createThumbnail($localPath, $thumbnailPaths['fs']); } return $name; } /** * Updates the file with the given name in the given path with the data. * * @param string $name The name * @param string $path The folder * @param string $data The data * * @return void * * @since 4.0.0 * @throws \Exception */ public function updateFile(string $name, string $path, $data) { $localPath = $this->getLocalPath($path . '/' . $name); if (!is_file($localPath)) { throw new FileNotFoundException(); } $this->checkContent($localPath, $data); File::write($localPath, $data); if ($this->thumbnails && MediaHelper::isImage(pathinfo($localPath)['basename'])) { $thumbnailPaths = $this->getLocalThumbnailPaths($localPath); if (empty($thumbnailPaths['fs'])) { return; } // Create the thumbnail $this->createThumbnail($localPath, $thumbnailPaths['fs']); } } /** * Deletes the folder or file of the given path. * * @param string $path The path to the file or folder * * @return void * * @since 4.0.0 * @throws \Exception */ public function delete(string $path) { $localPath = $this->getLocalPath($path); $thumbnailPaths = $this->getLocalThumbnailPaths($localPath); if (is_file($localPath)) { if ($this->thumbnails && !empty($thumbnailPaths['fs']) && is_file($thumbnailPaths['fs'])) { File::delete($thumbnailPaths['fs']); } $success = File::delete($localPath); } else { if (!Folder::exists($localPath)) { throw new FileNotFoundException(); } $success = Folder::delete($localPath); if ($this->thumbnails && !empty($thumbnailPaths['fs']) && is_dir($thumbnailPaths['fs'])) { Folder::delete($thumbnailPaths['fs']); } } if (!$success) { throw new \Exception('Delete not possible!'); } } /** * Returns the folder or file information for the given path. The returned object * has the following properties: * - type: The type can be file or dir * - name: The name of the file * - path: The relative path to the root * - extension: The file extension * - size: The size of the file * - create_date: The date created * - modified_date: The date modified * - mime_type: The mime type * - width: The width, when available * - height: The height, when available * - thumb_path: The thumbnail path of file, when available * * @param string $path The folder * * @return \stdClass * * @since 4.0.0 */ private function getPathInformation(string $path): \stdClass { // Prepare the path $path = Path::clean($path, '/'); // The boolean if it is a dir $isDir = is_dir($path); $createDate = $this->getDate(filectime($path)); $modifiedDate = $this->getDate(filemtime($path)); // Set the values $obj = new \stdClass(); $obj->type = $isDir ? 'dir' : 'file'; $obj->name = $this->getFileName($path); $obj->path = str_replace($this->rootPath, '', $path); $obj->extension = !$isDir ? File::getExt($obj->name) : ''; $obj->size = !$isDir ? filesize($path) : ''; $obj->mime_type = !$isDir ? (string) MediaHelper::getMimeType($path, MediaHelper::isImage($obj->name)) : ''; $obj->width = 0; $obj->height = 0; // Dates $obj->create_date = $createDate->format('c', true); $obj->create_date_formatted = HTMLHelper::_('date', $createDate, Text::_('DATE_FORMAT_LC5')); $obj->modified_date = $modifiedDate->format('c', true); $obj->modified_date_formatted = HTMLHelper::_('date', $modifiedDate, Text::_('DATE_FORMAT_LC5')); if ($obj->mime_type === 'image/svg+xml' && $obj->extension === 'svg') { $obj->thumb_path = $this->getUrl($obj->path); return $obj; } if (!$isDir && MediaHelper::isImage($obj->name)) { // Get the image properties try { $props = Image::getImageFileProperties($path); $obj->width = $props->width; $obj->height = $props->height; $obj->thumb_path = $this->thumbnails ? $this->getThumbnail($path) : $this->getUrl($obj->path); } catch (UnparsableImageException $e) { // Ignore the exception - it's an image that we don't know how to parse right now } } return $obj; } /** * Returns a Date with the correct Joomla timezone for the given date. * * @param string $date The date to create a Date from * * @return Date * * @since 4.0.0 */ private function getDate($date = null): Date { $dateObj = Factory::getDate($date); $timezone = Factory::getApplication()->get('offset'); $user = Factory::getUser(); if ($user->id) { $userTimezone = $user->getParam('timezone'); if (!empty($userTimezone)) { $timezone = $userTimezone; } } if ($timezone) { $dateObj->setTimezone(new \DateTimeZone($timezone)); } return $dateObj; } /** * Copies a file or folder from source to destination. * * It returns the new destination path. This allows the implementation * classes to normalise the file name. * * @param string $sourcePath The source path * @param string $destinationPath The destination path * @param bool $force Force to overwrite * * @return string * * @since 4.0.0 * @throws \Exception */ public function copy(string $sourcePath, string $destinationPath, bool $force = false): string { // Get absolute paths from relative paths $sourcePath = Path::clean($this->getLocalPath($sourcePath), '/'); $destinationPath = Path::clean($this->getLocalPath($destinationPath), '/'); if (!file_exists($sourcePath)) { throw new FileNotFoundException(); } $name = $this->getFileName($destinationPath); $safeName = $this->getSafeName($name); // If the safe name is different normalise the file name if ($safeName != $name) { $destinationPath = substr($destinationPath, 0, -\strlen($name)) . '/' . $safeName; } // Check for existence of the file in destination // if it does not exists simply copy source to destination if (is_dir($sourcePath)) { $this->copyFolder($sourcePath, $destinationPath, $force); } else { $this->copyFile($sourcePath, $destinationPath, $force); } // Get the relative path $destinationPath = str_replace($this->rootPath, '', $destinationPath); return $destinationPath; } /** * Copies a file * * @param string $sourcePath Source path of the file or directory * @param string $destinationPath Destination path of the file or directory * @param bool $force Set true to overwrite files or directories * * @return void * * @since 4.0.0 * @throws \Exception */ private function copyFile(string $sourcePath, string $destinationPath, bool $force = false) { if (is_dir($destinationPath)) { // If the destination is a folder we create a file with the same name as the source $destinationPath = $destinationPath . '/' . $this->getFileName($sourcePath); } if (!MediaHelper::checkFileExtension(pathinfo($destinationPath, PATHINFO_EXTENSION))) { throw new \Exception(Text::_('COM_MEDIA_MOVE_FILE_EXTENSION_INVALID')); } if (file_exists($destinationPath) && !$force) { throw new \Exception('Copy file is not possible as destination file already exists'); } if (!File::copy($sourcePath, $destinationPath)) { throw new \Exception('Copy file is not possible'); } } /** * Copies a folder * * @param string $sourcePath Source path of the file or directory * @param string $destinationPath Destination path of the file or directory * @param bool $force Set true to overwrite files or directories * * @return void * * @since 4.0.0 * @throws \Exception */ private function copyFolder(string $sourcePath, string $destinationPath, bool $force = false) { if (file_exists($destinationPath) && !$force) { throw new \Exception('Copy folder is not possible as destination folder already exists'); } if (is_file($destinationPath) && !File::delete($destinationPath)) { throw new \Exception('Copy folder is not possible as destination folder is a file and can not be deleted'); } if (!Folder::copy($sourcePath, $destinationPath, '', $force)) { throw new \Exception('Copy folder is not possible'); } } /** * Moves a file or folder from source to destination. * * It returns the new destination path. This allows the implementation * classes to normalise the file name. * * @param string $sourcePath The source path * @param string $destinationPath The destination path * @param bool $force Force to overwrite * * @return string * * @since 4.0.0 * @throws \Exception */ public function move(string $sourcePath, string $destinationPath, bool $force = false): string { // Get absolute paths from relative paths $sourcePath = Path::clean($this->getLocalPath($sourcePath), '/'); $destinationPath = Path::clean($this->getLocalPath($destinationPath), '/'); if (!file_exists($sourcePath)) { throw new FileNotFoundException(); } $name = $this->getFileName($destinationPath); $safeName = $this->getSafeName($name); // If transliterating could not happen, and all characters except of the file extension are filtered out, then throw an error. if ($safeName === pathinfo($sourcePath, PATHINFO_EXTENSION)) { throw new \Exception(Text::_('COM_MEDIA_ERROR_MAKESAFE')); } // If the safe name is different normalise the file name if ($safeName != $name) { $destinationPath = substr($destinationPath, 0, -\strlen($name)) . $safeName; } if (is_dir($sourcePath)) { $this->moveFolder($sourcePath, $destinationPath, $force); } else { $this->moveFile($sourcePath, $destinationPath, $force); } // Get the relative path $destinationPath = str_replace($this->rootPath, '', $destinationPath); return $destinationPath; } /** * Moves a file * * @param string $sourcePath Absolute path of source * @param string $destinationPath Absolute path of destination * @param bool $force Set true to overwrite file if exists * * @return void * * @since 4.0.0 * @throws \Exception */ private function moveFile(string $sourcePath, string $destinationPath, bool $force = false) { if (is_dir($destinationPath)) { // If the destination is a folder we create a file with the same name as the source $destinationPath = $destinationPath . '/' . $this->getFileName($sourcePath); } if (!MediaHelper::checkFileExtension(pathinfo($destinationPath, PATHINFO_EXTENSION))) { throw new \Exception('Move file is not possible as the extension is invalid'); } if (file_exists($destinationPath) && !$force) { throw new \Exception('Move file is not possible as destination file already exists'); } if (!File::move($sourcePath, $destinationPath)) { throw new \Exception('Move file is not possible'); } } /** * Moves a folder from source to destination * * @param string $sourcePath Source path of the file or directory * @param string $destinationPath Destination path of the file or directory * @param bool $force Set true to overwrite files or directories * * @return void * * @since 4.0.0 * @throws \Exception */ private function moveFolder(string $sourcePath, string $destinationPath, bool $force = false) { if (file_exists($destinationPath) && !$force) { throw new \Exception('Move folder is not possible as destination folder already exists'); } if (is_file($destinationPath) && !File::delete($destinationPath)) { throw new \Exception('Move folder is not possible as destination folder is a file and can not be deleted'); } if (is_dir($destinationPath)) { // We need to bypass exception thrown in JFolder when destination exists // So we only copy it in forced condition, then delete the source to simulate a move if (!Folder::copy($sourcePath, $destinationPath, '', true)) { throw new \Exception('Move folder to an existing destination failed'); } // Delete the source Folder::delete($sourcePath); return; } // Perform usual moves $value = Folder::move($sourcePath, $destinationPath); if ($value !== true) { throw new \Exception($value); } } /** * Returns a url which can be used to display an image from within the "images" directory. * * @param string $path Path of the file relative to adapter * * @return string * * @since 4.0.0 */ public function getUrl(string $path): string { return Uri::root() . $this->getEncodedPath($this->filePath . $path); } /** * Returns the name of this adapter. * * @return string * * @since 4.0.0 */ public function getAdapterName(): string { return $this->filePath; } /** * Search for a pattern in a given path * * @param string $path The base path for the search * @param string $needle The path to file * @param bool $recursive Do a recursive search * * @return \stdClass[] * * @since 4.0.0 */ public function search(string $path, string $needle, bool $recursive = false): array { $pattern = Path::clean($this->getLocalPath($path) . '/*' . $needle . '*'); if ($recursive) { $results = $this->rglob($pattern); } else { $results = glob($pattern); } $searchResults = []; foreach ($results as $result) { $searchResults[] = $this->getPathInformation($result); } return $searchResults; } /** * Do a recursive search on a given path * * @param string $pattern The pattern for search * @param int $flags Flags for search * * @return array * * @since 4.0.0 */ private function rglob(string $pattern, int $flags = 0): array { $files = glob($pattern, $flags); foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { $files = array_merge($files, $this->rglob($dir . '/' . $this->getFileName($pattern), $flags)); } return $files; } /** * Replace spaces on a path with %20 * * @param string $path The Path to be encoded * * @return string * * @since 4.0.0 * @throws FileNotFoundException */ private function getEncodedPath(string $path): string { return str_replace(" ", "%20", $path); } /** * Creates a safe file name for the given name. * * @param string $name The filename * * @return string * * @since 4.0.0 * @throws \Exception */ private function getSafeName(string $name): string { // Make the filename safe if (!$name = File::makeSafe($name)) { throw new \Exception(Text::_('COM_MEDIA_ERROR_MAKESAFE')); } // Transform filename to punycode $name = PunycodeHelper::toPunycode($name); // Get the extension $extension = File::getExt($name); // Normalise extension, always lower case if ($extension) { $extension = '.' . strtolower($extension); } $nameWithoutExtension = substr($name, 0, \strlen($name) - \strlen($extension)); return $nameWithoutExtension . $extension; } /** * Performs various check if it is allowed to save the content with the given name. * * @param string $localPath The local path * @param string $mediaContent The media content * * @return void * * @since 4.0.0 * @throws \Exception */ private function checkContent(string $localPath, string $mediaContent) { $name = $this->getFileName($localPath); // The helper $helper = new MediaHelper(); // @todo find a better way to check the input, by not writing the file to the disk $tmpFile = Path::clean(\dirname($localPath) . '/' . uniqid() . '.' . strtolower(File::getExt($name))); if (!File::write($tmpFile, $mediaContent)) { throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 500); } $can = $helper->canUpload(['name' => $name, 'size' => \strlen($mediaContent), 'tmp_name' => $tmpFile], 'com_media'); File::delete($tmpFile); if (!$can) { throw new \Exception(Text::_('JLIB_MEDIA_ERROR_UPLOAD_INPUT'), 403); } } /** * Returns the file name of the given path. * * @param string $path The path * * @return string * * @since 4.0.0 * @throws \Exception */ private function getFileName(string $path): string { $path = Path::clean($path); // Basename does not work here as it strips out certain characters like upper case umlaut u $path = explode(DIRECTORY_SEPARATOR, $path); // Return the last element return array_pop($path); } /** * Returns the local filesystem path for the given path. * * Throws an InvalidPathException if the path is invalid. * * @param string $path The path * * @return string * * @since 4.0.0 * @throws InvalidPathException */ private function getLocalPath(string $path): string { try { return Path::check($this->rootPath . '/' . $path); } catch (\Exception $e) { throw new InvalidPathException($e->getMessage()); } } /** * Returns the local filesystem thumbnail path for the given path. * * Throws an InvalidPathException if the path is invalid. * * @param string $path The path * * @return array * * @since 4.3.0 * @throws InvalidPathException */ private function getLocalThumbnailPaths(string $path): array { $rootPath = str_replace(['\\', '/'], '/', $this->rootPath); $path = str_replace(['\\', '/'], '/', $path); try { $fs = Path::check(str_replace($rootPath, JPATH_ROOT . '/media/cache/com_media/thumbs/' . $this->filePath, $path)); $url = str_replace($rootPath, 'media/cache/com_media/thumbs/' . $this->filePath, $path); return [ 'fs' => $fs, 'url' => $url, ]; } catch (\Exception $e) { throw new InvalidPathException($e->getMessage()); } } /** * Returns the path for the thumbnail of the given image. * If the thumbnail does not exist, it will be created. * * @param string $path The path of the image * * @return string * * @since 4.3.0 */ private function getThumbnail(string $path): string { $thumbnailPaths = $this->getLocalThumbnailPaths($path); if (empty($thumbnailPaths['fs'])) { return $this->getUrl($path); } $dir = dirname($thumbnailPaths['fs']); if (!is_dir($dir)) { Folder::create($dir); } // Create the thumbnail if (!is_file($thumbnailPaths['fs']) && !$this->createThumbnail($path, $thumbnailPaths['fs'])) { return $this->getUrl($path); } return Uri::root() . $this->getEncodedPath($thumbnailPaths['url']); } /** * Create a thumbnail of the given image. * * @param string $path The path of the image * @param string $thumbnailPath The path of the thumbnail * * @return boolean * * @since 4.3.0 */ private function createThumbnail(string $path, string $thumbnailPath): bool { $image = new Image($path); try { $image->createThumbnails([$this->thumbnailSize[0] . 'x' . $this->thumbnailSize[1]], $image::SCALE_INSIDE, dirname($thumbnailPath), true); } catch (\Exception $e) { return false; } return true; } } entities/task_log.php 0000604 00000034752 15062303320 0010705 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_admin\reportbuilder\local\entities; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\duration; use core_reportbuilder\local\filters\number; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\filters\text; use core_reportbuilder\local\filters\autocomplete; use core_reportbuilder\local\helpers\format; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use stdClass; use core_collator; /** * Task log entity class implementation * * @package core_admin * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class task_log extends base { /** @var int Result success */ protected const SUCCESS = 0; /** @var int Result failed */ protected const FAILED = 1; /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['task_log' => 'tl']; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('entitytasklog', 'admin'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $tablealias = $this->get_table_alias('task_log'); // Name column. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("$tablealias.classname") ->set_is_sortable(true) ->add_callback(static function(string $classname): string { $output = ''; if (class_exists($classname)) { $task = new $classname; if ($task instanceof \core\task\task_base) { $output = $task->get_name(); } } $output .= \html_writer::tag('div', "\\{$classname}", [ 'class' => 'small text-muted', ]); return $output; }); // Component column. $columns[] = (new column( 'component', new lang_string('plugin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.component") ->set_is_sortable(true); // Type column. $columns[] = (new column( 'type', new lang_string('tasktype', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.type") ->set_is_sortable(true) ->add_callback(static function($value): string { if (\core\task\database_logger::TYPE_SCHEDULED === (int) $value) { return get_string('task_type:scheduled', 'admin'); } return get_string('task_type:adhoc', 'admin'); }); // Start time column. $columns[] = (new column( 'starttime', new lang_string('task_starttime', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tablealias}.timestart") ->set_is_sortable(true) ->add_callback([format::class, 'userdate'], get_string('strftimedatetimeshortaccurate', 'core_langconfig')); // End time column. $columns[] = (new column( 'endtime', new lang_string('task_endtime', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tablealias}.timeend") ->set_is_sortable(true) ->add_callback([format::class, 'userdate'], get_string('strftimedatetimeshortaccurate', 'core_langconfig')); // Duration column. $columns[] = (new column( 'duration', new lang_string('task_duration', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_FLOAT) ->add_field("{$tablealias}.timeend - {$tablealias}.timestart", 'duration') ->set_is_sortable(true) ->add_callback(static function(float $value): string { $duration = round($value, 2); if (empty($duration)) { // The format_time function returns 'now' when the difference is exactly 0. // Note: format_time performs concatenation in exactly this fashion so we should do this for consistency. return '0 ' . get_string('secs', 'moodle'); } return format_time($duration); }); // Hostname column. $columns[] = (new column( 'hostname', new lang_string('hostname', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("$tablealias.hostname") ->set_is_sortable(true); // PID column. $columns[] = (new column( 'pid', new lang_string('pid', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("{$tablealias}.pid") ->set_is_sortable(true) // Although this is an integer column, it doesn't make sense to perform numeric aggregation on it. ->set_disabled_aggregation(['avg', 'count', 'countdistinct', 'max', 'min', 'sum']); // Database column. $columns[] = (new column( 'database', new lang_string('task_dbstats', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$tablealias}.dbreads, {$tablealias}.dbwrites") ->set_is_sortable(true, ["{$tablealias}.dbreads", "{$tablealias}.dbwrites"]) ->add_callback(static function(int $value, stdClass $row): string { $output = ''; $output .= \html_writer::div(get_string('task_stats:dbreads', 'admin', $row->dbreads)); $output .= \html_writer::div(get_string('task_stats:dbwrites', 'admin', $row->dbwrites)); return $output; }) // Although this is an integer column, it doesn't make sense to perform numeric aggregation on it. ->set_disabled_aggregation(['avg', 'count', 'countdistinct', 'max', 'min', 'sum']); // Database reads column. $columns[] = (new column( 'dbreads', new lang_string('task_dbreads', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$tablealias}.dbreads") ->set_is_sortable(true); // Database writes column. $columns[] = (new column( 'dbwrites', new lang_string('task_dbwrites', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$tablealias}.dbwrites") ->set_is_sortable(true); // Result column. $columns[] = (new column( 'result', new lang_string('task_result', 'admin'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) // For accurate aggregation, we need to return boolean success = true by xor'ing the field value. ->add_field($DB->sql_bitxor("{$tablealias}.result", 1), 'success') ->set_is_sortable(true) ->add_callback(static function(bool $success): string { if (!$success) { return get_string('task_result:failed', 'admin'); } return get_string('success'); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { global $DB; $tablealias = $this->get_table_alias('task_log'); // Name filter (Filter by classname). $filters[] = (new filter( autocomplete::class, 'name', new lang_string('classname', 'tool_task'), $this->get_entity_name(), "{$tablealias}.classname" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { global $DB; $classnames = $DB->get_fieldset_sql('SELECT DISTINCT classname FROM {task_log} ORDER BY classname ASC'); $options = []; foreach ($classnames as $classname) { if (class_exists($classname)) { $task = new $classname; $options[$classname] = $task->get_name(); } } core_collator::asort($options); return $options; }); // Component filter. $filters[] = (new filter( text::class, 'component', new lang_string('plugin'), $this->get_entity_name(), "{$tablealias}.component" )) ->add_joins($this->get_joins()); // Type filter. $filters[] = (new filter( select::class, 'type', new lang_string('tasktype', 'admin'), $this->get_entity_name(), "{$tablealias}.type" )) ->add_joins($this->get_joins()) ->set_options([ \core\task\database_logger::TYPE_ADHOC => new lang_string('task_type:adhoc', 'admin'), \core\task\database_logger::TYPE_SCHEDULED => new lang_string('task_type:scheduled', 'admin'), ]); // Output filter (Filter by task output). $filters[] = (new filter( text::class, 'output', new lang_string('task_logoutput', 'admin'), $this->get_entity_name(), $DB->sql_cast_to_char("{$tablealias}.output") )) ->add_joins($this->get_joins()); // Start time filter. $filters[] = (new filter( date::class, 'timestart', new lang_string('task_starttime', 'admin'), $this->get_entity_name(), "{$tablealias}.timestart" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_PREVIOUS, date::DATE_CURRENT, ]); // End time. $filters[] = (new filter( date::class, 'timeend', new lang_string('task_endtime', 'admin'), $this->get_entity_name(), "{$tablealias}.timeend" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_PREVIOUS, date::DATE_CURRENT, ]); // Duration filter. $filters[] = (new filter( duration::class, 'duration', new lang_string('task_duration', 'admin'), $this->get_entity_name(), "{$tablealias}.timeend - {$tablealias}.timestart" )) ->add_joins($this->get_joins()); // Database reads. $filters[] = (new filter( number::class, 'dbreads', new lang_string('task_dbreads', 'admin'), $this->get_entity_name(), "{$tablealias}.dbreads" )) ->add_joins($this->get_joins()); // Database writes. $filters[] = (new filter( number::class, 'dbwrites', new lang_string('task_dbwrites', 'admin'), $this->get_entity_name(), "{$tablealias}.dbwrites" )) ->add_joins($this->get_joins()); // Result filter. $filters[] = (new filter( select::class, 'result', new lang_string('task_result', 'admin'), $this->get_entity_name(), "{$tablealias}.result" )) ->add_joins($this->get_joins()) ->set_options([ self::SUCCESS => get_string('success'), self::FAILED => get_string('task_result:failed', 'admin'), ]); return $filters; } } systemreports/task_logs.php 0000604 00000012562 15062303320 0012202 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_admin\reportbuilder\local\systemreports; use context_system; use core_admin\reportbuilder\local\entities\task_log; use core_reportbuilder\local\entities\user; use core_reportbuilder\local\report\action; use lang_string; use moodle_url; use pix_icon; use core_reportbuilder\system_report; /** * Task logs system report class implementation * * @package core_admin * @copyright 2021 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class task_logs extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { // Our main entity, it contains all of the column definitions that we need. $entitymain = new task_log(); $entitymainalias = $entitymain->get_table_alias('task_log'); $this->set_main_table('task_log', $entitymainalias); $this->add_entity($entitymain); // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entitymainalias}.id"); // We can join the "user" entity to our "main" entity and use the fullname column from the user entity. $entityuser = new user(); $entituseralias = $entityuser->get_table_alias('user'); $this->add_entity($entityuser->add_join( "LEFT JOIN {user} {$entituseralias} ON {$entituseralias}.id = {$entitymainalias}.userid" )); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); $this->add_actions(); // Set if report can be downloaded. $this->set_downloadable(true, get_string('tasklogs', 'admin')); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_capability('moodle/site:config', context_system::instance()); } /** * Get the visible name of the report * * @return string */ public static function get_name(): string { return get_string('entitytasklog', 'admin'); } /** * Adds the columns we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ public function add_columns(): void { $columns = [ 'task_log:name', 'task_log:type', 'user:fullname', 'task_log:starttime', 'task_log:duration', 'task_log:hostname', 'task_log:pid', 'task_log:database', 'task_log:result', ]; $this->add_columns_from_entities($columns); // It's possible to override the display name of a column, if you don't want to use the value provided by the entity. if ($column = $this->get_column('user:fullname')) { $column->set_title(new lang_string('user', 'admin')); } // It's possible to set a default initial sort direction for one column. $this->set_initial_sort_column('task_log:starttime', SORT_DESC); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $filters = [ 'task_log:name', 'task_log:type', 'task_log:output', 'task_log:result', 'task_log:timestart', 'task_log:duration', ]; $this->add_filters_from_entities($filters); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":id" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { // Action to view individual task log on a popup window. $this->add_action((new action( new moodle_url('/admin/tasklogs.php', ['logid' => ':id']), new pix_icon('e/search', ''), [], true, new lang_string('view'), ))); // Action to download individual task log. $this->add_action((new action( new moodle_url('/admin/tasklogs.php', ['logid' => ':id', 'download' => true]), new pix_icon('t/download', ''), [], false, new lang_string('download'), ))); } } command_test.php 0000604 00000030477 15062353612 0007744 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use ReflectionMethod; defined('MOODLE_INTERNAL') || die(); require_once(dirname(__DIR__) . '/matrix_client_test_trait.php'); /** * Tests for the Matrix command class. * * @package communication_matrix * @category test * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \communication_matrix\local\command * @coversDefaultClass \communication_matrix\local\command */ class command_test extends \advanced_testcase { use \communication_matrix\matrix_client_test_trait; /** * Test instantiation of a command when no method is provided. */ public function test_standard_instantiation(): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $command = new command( $instance, method: 'PUT', endpoint: 'example/endpoint', ); // Check the standard functionality. $this->assertEquals('/example/endpoint', $command->getUri()->getPath()); $this->assertEquals('PUT', $command->getMethod()); $this->assertArrayHasKey('Authorization', $command->getHeaders()); } /** * Test instantiation of a command when no method is provided. */ public function test_instantiation_without_auth(): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $command = new command( $instance, method: 'PUT', endpoint: 'example/endpoint', requireauthorization: false, ); // Check the standard functionality. $this->assertEquals('/example/endpoint', $command->getUri()->getPath()); $this->assertEquals('PUT', $command->getMethod()); $this->assertArrayNotHasKey('Authorization', $command->getHeaders()); } /** * Test processing of command URL properties. * * @dataProvider url_parsing_provider * @param string $url * @param array $params * @param string $expected */ public function test_url_parsing( string $url, array $params, string $expected, ): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $command = new command( $instance, method: 'PUT', endpoint: $url, params: $params, ); $this->assertEquals($expected, $command->getUri()->getPath()); } /** * Data provider for url parsing tests. * * @return array */ public static function url_parsing_provider(): array { return [ [ 'example/:id/endpoint', [':id' => '39492'], '/example/39492/endpoint', ], [ 'example/:id/endpoint/:id', [':id' => '39492'], '/example/39492/endpoint/39492', ], [ 'example/:id/endpoint/:id/:name', [ ':id' => '39492', ':name' => 'matrix', ], '/example/39492/endpoint/39492/matrix', ], ]; } /** * Test processing of command URL properties with an array which contains untranslated parameters. */ public function test_url_parsing_extra_properties(): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $this->expectException(\OutOfRangeException::class); $this->expectExceptionMessage("URL contains untranslated parameters 'example/:id/endpoint'"); new command( $instance, method: 'PUT', endpoint: 'example/:id/endpoint', ); } /** * Test processing of command URL properties with an array which contains untranslated parameters. */ public function test_url_parsing_unused_properites(): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $this->expectException(\OutOfRangeException::class); $this->expectExceptionMessage("Parameter not found in URL ':id'"); new command( $instance, method: 'PUT', endpoint: 'example/:ids/endpoint', params: [ ':id' => 12345, ], ); } /** * Test the parameter fetching, processing, and parsing. * * @dataProvider parameter_and_option_provider * @param string $endpoint * @param array $params * @param array $remainingparams * @param array $allparams * @param array $options */ public function test_parameters( string $endpoint, array $params, array $remainingparams, array $allparams, array $options, ): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $command = new command( $instance, method: 'PUT', endpoint: $endpoint, params: $params, ); $this->assertSame($remainingparams, $command->get_remaining_params()); $this->assertSame($allparams, $command->get_all_params()); $this->assertSame($options, $command->get_options()); } /** * Data provider for parameter tests. * * @return array */ public static function parameter_and_option_provider(): array { $command = [ 'method' => 'PUT', 'endpoint' => 'example/:id/endpoint', ]; return [ 'no parameters' => [ 'endpoint' => 'example/endpoint', 'params' => [], 'remainingparams' => [], 'allparams' => [], 'options' => [ 'json' => [], ], ], 'named params' => [ 'endpoint' => 'example/:id/endpoint', 'params' => [ ':id' => 12345, ], 'remainingparams' => [], 'allparams' => [ ':id' => 12345, ], 'options' => [ 'json' => [], ], ], 'mixture of params' => [ 'endpoint' => 'example/:id/endpoint', 'params' => [ ':id' => 12345, 'name' => 'matrix', ], 'remainingparams' => [ 'name' => 'matrix', ], 'allparams' => [ ':id' => 12345, 'name' => 'matrix', ], 'options' => [ 'json' => [ 'name' => 'matrix', ], ], ], ]; } /** * Test the query parameter handling. * * @dataProvider query_provider * @param array $query * @param string $expected */ public function test_query_parameters( array $query, string $expected, ): void { // The query parameter is only added at the time we call send. // That's because it can only be provided to Guzzle as an Option, not as part of the URL. // Options can only be applied at time of transfer. // Unfortuantely that leads to slightly less ideal testing that we'd like here. $mock = new MockHandler(); $instance = $this->get_mocked_instance_for_version( 'v1.7', mock: $mock, ); $mock->append(function (Request $request) use ($expected): Response { $this->assertSame( $expected, $request->getUri()->getQuery(), ); return new Response(); }); $command = new command( $instance, method: 'PUT', endpoint: 'example/endpoint', query: $query, ); $execute = new ReflectionMethod($instance, 'execute'); $execute->setAccessible(true); $execute->invoke($instance, $command); } /** * Data provider for query parameter tests. * @return array */ public static function query_provider(): array { return [ 'no query' => [ 'query' => [], 'expected' => '', ], 'single query' => [ 'query' => [ 'name' => 'matrix', ], 'expected' => 'name=matrix', ], 'multiple queries' => [ 'query' => [ 'name' => 'matrix', 'type' => 'room', ], 'expected' => 'name=matrix&type=room', ], ]; } /** * Test the sendasjson constructor parameter. * * @dataProvider sendasjson_provider * @param bool $sendasjson * @param string $endpoint * @param array $params * @param array $remainingparams * @param array $allparams * @param array $expectedoptions */ public function test_send_as_json( bool $sendasjson, string $endpoint, array $params, array $remainingparams, array $allparams, array $expectedoptions, ): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $command = new command( $instance, method: 'PUT', endpoint: $endpoint, params: $params, sendasjson: $sendasjson, ); $this->assertSame($remainingparams, $command->get_remaining_params()); $this->assertSame($allparams, $command->get_all_params()); $this->assertSame($expectedoptions, $command->get_options()); } /** * Test the sendasjosn option to the command constructor. * * @return array */ public static function sendasjson_provider(): array { return [ 'As JSON' => [ 'sendasjon' => true, 'endpoint' => 'example/:id/endpoint', 'params' => [ ':id' => 12345, 'name' => 'matrix', ], 'remainingparams' => [ 'name' => 'matrix', ], 'allparams' => [ ':id' => 12345, 'name' => 'matrix', ], 'expectedoptions' => [ 'json' => [ 'name' => 'matrix', ], ], ], 'Not as JSON' => [ 'sendasjson' => false, 'endpoint' => 'example/:id/endpoint', 'params' => [ ':id' => 12345, 'name' => 'matrix', ], 'remainingparams' => [ 'name' => 'matrix', ], 'allparams' => [ ':id' => 12345, 'name' => 'matrix', ], 'expectedoptions' => [ ], ], ]; } /** * Test the sendasjosn option to the command constructor. */ public function test_ignorehttperrors(): void { $instance = $this->get_mocked_instance_for_version('v1.7'); $command = new command( $instance, method: 'PUT', endpoint: 'example/endpoint', ignorehttperrors: true, ); $options = $command->get_options(); $this->assertArrayHasKey('http_errors', $options); $this->assertFalse($options['http_errors']); } } spec/v1p2.php 0000604 00000002130 15062431010 0006757 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.2 of the Matrix specification. * * https://spec.matrix.org/v1.2/client-server-api/ * https://spec.matrix.org/v1.2/changelog/#api-changes * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p2 extends v1p1 { } spec/v1p5.php 0000604 00000002130 15062431010 0006762 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.5 of the Matrix specification. * * https://spec.matrix.org/v1.5/client-server-api/ * https://spec.matrix.org/v1.5/changelog/#api-changes * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p5 extends v1p4 { } spec/v1p4.php 0000604 00000002130 15062431010 0006761 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.4 of the Matrix specification. * * https://spec.matrix.org/v1.4/client-server-api/ * https://spec.matrix.org/v1.4/changelog/#api-changes * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p4 extends v1p3 { } spec/v1p3.php 0000604 00000002130 15062431010 0006760 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.3 of the Matrix specification. * * https://spec.matrix.org/v1.3/client-server-api/ * https://spec.matrix.org/v1.3/changelog/#api-changes * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p3 extends v1p2 { } spec/v1p7.php 0000604 00000002435 15062431010 0006774 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.7 of the Matrix specification. * * https://spec.matrix.org/v1.7/client-server-api/ * https://spec.matrix.org/v1.7/changelog/#api-changes * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p7 extends v1p6 { // Note: A new Content Upload API was introduced. // See details in the spec: // https://github.com/matrix-org/matrix-spec-proposals/pull/2246. use features\matrix\media_create_v1; } spec/features/synapse/invite_member_to_room_v1.php 0000604 00000003557 15062431010 0016476 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\synapse; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Synapse API feature to invite a user into a room. * * https://matrix-org.github.io/synapse/latest/admin_api/room_membership.html#edit-room-membership-api * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait invite_member_to_room_v1 { /** * Join a user to a room. * * Note: This joins the user, and does not invite them. * * @param string $roomid * @param string $userid * @return Response */ public function invite_member_to_room(string $roomid, string $userid): Response { $params = [ ':roomid' => $roomid, 'user_id' => $userid, ]; return $this->execute(new command( $this, method: 'POST', endpoint: '_synapse/admin/v1/join/:roomid', params: $params, )); } } spec/features/synapse/create_user_v2.php 0000604 00000004203 15062431010 0014402 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\synapse; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Synapse API feature for creating a user. * * https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait create_user_v2 { /** * Create a new user. * * @param string $userid The Matrix user id. * @param string $displayname The visible name of the user * @param array $threepids The third-party identifiers of the user. * @param null|array $externalids */ public function create_user( string $userid, string $displayname, array $threepids, ?array $externalids = null, ): Response { $params = [ ':userid' => $userid, 'displayname' => $displayname, 'threepids' => $threepids, ]; if ($externalids !== null) { $params['externalids'] = $externalids; } return $this->execute(new command( $this, method: 'PUT', endpoint: '_synapse/admin/v2/users/:userid', params: $params, )); } } spec/features/synapse/get_user_info_v2.php 0000604 00000003320 15062431010 0014730 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\synapse; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Synapse API feature for fetching info about a user. * * https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#query-user-account * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait get_user_info_v2 { /** * Get user info. * * @param string $userid * @return Response */ public function get_user_info(string $userid): Response { return $this->execute(new command( $this, method: 'GET', endpoint: '_synapse/admin/v2/users/:userid', ignorehttperrors: true, params: [ ':userid' => $userid, ], )); } } spec/features/synapse/get_room_info_v1.php 0000604 00000003231 15062431010 0014726 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\synapse; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Synapse API feature for fetching room info. * * https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-details-api * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait get_room_info_v1 { /** * Get room info. * * @param string $roomid * @return Response */ public function get_room_info(string $roomid): Response { return $this->execute(new command( $this, method: 'GET', endpoint: '_synapse/admin/v1/rooms/:roomid', params: [ ':roomid' => $roomid, ], )); } } spec/features/matrix/upload_content_v3.php 0000604 00000005640 15062431010 0014750 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Utils; /** * Matrix API feature to upload content. * * https://spec.matrix.org/v1.1/client-server-api/#post_matrixmediav3upload * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait upload_content_v3 { /** * Upload the content in the matrix/synapse server. * * @param null|\stored_file $content The content to be uploaded * @param null|string $mediaid The mediaid to associate a file with. Supported for v1.7 API an above only. * @return Response */ public function upload_content( ?\stored_file $content, ?string $mediaid = null, ): Response { $query = []; if ($content) { $query['filename'] = $content->get_filename(); } if ($mediaid !== null) { // Specification of the mediaid requires version 1.7 or above of the upload API. // See https://spec.matrix.org/v1.7/client-server-api/#put_matrixmediav3uploadservernamemediaid. $this->requires_version('1.7'); $command = new command( $this, method: 'PUT', endpoint: '_matrix/media/v3/upload/:mediaid', sendasjson: false, query: $query, params: [ ':mediaid' => $mediaid, ], ); } else { $command = new command( $this, method: 'POST', endpoint: '_matrix/media/v3/upload', sendasjson: false, query: $query, ); } if ($content) { // Add the content-type, and header. $command = $command->withHeader('Content-Type', $content->get_mimetype()); $command = $command->withBody(Utils::streamFor($content->get_content())); } return $this->execute($command); } } spec/features/matrix/update_room_avatar_v3.php 0000604 00000003673 15062431010 0015612 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to update a room avatar. * * https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait update_room_avatar_v3 { /** * Set the avatar for a room to the specified URL. * * @param string $roomid The roomid to set for * @param null|string $avatarurl The mxc URL to use * @return Response */ public function update_room_avatar( string $roomid, ?string $avatarurl, ): Response { $params = [ ':roomid' => $roomid, 'url' => $avatarurl, ]; return $this->execute(new command( $this, method: 'PUT', endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.avatar', ignorehttperrors: true, params: $params, )); } } spec/features/matrix/remove_member_from_room_v3.php 0000604 00000003561 15062431010 0016635 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to remove a member from a room. * * https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidkick * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait remove_member_from_room_v3 { /** * Remove a member from a room. * * @param string $roomid The roomid to remove from * @param string $userid The member to remove * @return Response */ public function remove_member_from_room( string $roomid, string $userid, ): Response { $params = [ ':roomid' => $roomid, 'user_id' => $userid, ]; return $this->execute(new command( $this, method: 'POST', endpoint: '_matrix/client/v3/rooms/:roomid/kick', params: $params, )); } } spec/features/matrix/update_room_name_v3.php 0000604 00000003444 15062431010 0015250 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to update a room name. * * https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait update_room_name_v3 { /** * Set the name for a room. * * @param string $roomid * @param string $name * @return Response */ public function update_room_name(string $roomid, string $name): Response { $params = [ ':roomid' => $roomid, 'name' => $name, ]; return $this->execute(new command( $this, method: 'PUT', endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.name', params: $params, )); } } spec/features/matrix/update_room_topic_v3.php 0000604 00000003455 15062431010 0015450 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to update a room topic. * * https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait update_room_topic_v3 { /** * Set the topic for a room. * * @param string $roomid * @param string $topic * @return Response */ public function update_room_topic(string $roomid, string $topic): Response { $params = [ ':roomid' => $roomid, 'topic' => $topic, ]; return $this->execute(new command( $this, method: 'PUT', endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.topic', params: $params, )); } } spec/features/matrix/update_room_power_levels_v3.php 0000604 00000005566 15062431010 0017045 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use communication_matrix\matrix_constants; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to update a room power levels. * * Matrix rooms have a concept of power levels, which are used to determine what actions a user can perform in a room. * * https://spec.matrix.org/v1.1/client-server-api/#mroompower_levels * * @package communication_matrix * @copyright 2023 Safat Shahin <safat.shahin@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait update_room_power_levels_v3 { /** * Set the avatar for a room to the specified URL. * * @param string $roomid The roomid to set for * @param array $users The users to set power levels for * @param int $ban The level required to ban a user * @param int $invite The level required to invite a user * @param int $kick The level required to kick a user * @param array $notifications The level required to send notifications * @param int $redact The level required to redact events * @return Response */ public function update_room_power_levels( string $roomid, array $users, int $ban = matrix_constants::POWER_LEVEL_MAXIMUM, int $invite = matrix_constants::POWER_LEVEL_MODERATOR, int $kick = matrix_constants::POWER_LEVEL_MODERATOR, array $notifications = [ 'room' => matrix_constants::POWER_LEVEL_MODERATOR, ], int $redact = matrix_constants::POWER_LEVEL_MODERATOR, ): Response { $params = [ ':roomid' => $roomid, 'ban' => $ban, 'invite' => $invite, 'kick' => $kick, 'notifications' => $notifications, 'redact' => $redact, 'users' => $users, ]; return $this->execute(new command( $this, method: 'PUT', endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.power_levels', params: $params, )); } } spec/features/matrix/media_create_v1.php 0000604 00000003030 15062431010 0014321 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to create an mxc Media URI. * * https://spec.matrix.org/v1.1/client-server-api/#post_matrixmediav3upload * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait media_create_v1 { /** * Create a media URI. * * @return Response */ public function media_create(): Response { return $this->execute(new command( $this, method: 'POST', endpoint: '_matrix/media/v1/create', )); } } spec/features/matrix/create_room_v3.php 0000604 00000004654 15062431010 0014235 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature for room creation. * * https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3createroom * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait create_room_v3 { /** * Create a new room. * * @param string $name The room name * @param null|string $visibility The room visibility * @param null|string $preset The preset to use * @param null|array $initialstate Initial state variables * @param array $options Any additional options * @return Response */ public function create_room( string $name, ?string $visibility = null, ?string $preset = null, ?array $initialstate = null, array $options = [], ): Response { $params = [ 'name' => $name, ]; if ($visibility !== null) { $params['visibility'] = $visibility; } if ($preset !== null) { $params['preset'] = $preset; } if ($initialstate !== null) { $params['initial_state'] = $initialstate; } if (array_key_exists('topic', $options)) { $params['topic'] = $options['topic'] ?? ''; } return $this->execute(new command( $this, method: 'POST', endpoint: '_matrix/client/v3/createRoom', params: $params, )); } } spec/features/matrix/get_room_powerlevels_from_sync_v3.php 0000604 00000006215 15062431010 0020252 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to fetch room power levels using the sync API. * * https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3sync * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait get_room_powerlevels_from_sync_v3 { /** * Get a list of room members. * * @param string $roomid The room ID * @return Response */ public function get_room_power_levels(string $roomid): Response { // Filter the event data according to the API: // https://spec.matrix.org/v1.1/client-server-api/#filtering // We have to filter out all of the object data that we do not want, // and set a filter to only fetch the one room that we do want. $filter = (object) [ "account_data" => (object) [ // We don't want any account info for this call. "not_types" => ['*'], ], "event_fields" => [ // We only care about type, and content. Not sender. "type", "content", ], "event_format" => "client", "presence" => (object) [ // We don't need any presence data. "not_types" => ['*'], ], "room" => (object) [ // We only want state information for power levels, not timeline and ephemeral data. "rooms" => [ $roomid, ], "state" => (object) [ "types" => [ "m.room.power_levels", ], ], "ephemeral" => (object) [ "not_types" => ['*'], ], "timeline" => (object) [ "not_types" => ['*'], ], ], ]; $query = [ 'filter' => json_encode($filter), ]; return $this->execute(new command( $this, method: 'GET', endpoint: '_matrix/client/v3/sync', query: $query, sendasjson: false, )); } } spec/features/matrix/get_room_members_v3.php 0000604 00000003354 15062431010 0015257 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec\features\matrix; use communication_matrix\local\command; use GuzzleHttp\Psr7\Response; /** * Matrix API feature to fetch a list of room members. * * https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidjoined_members * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait get_room_members_v3 { /** * Get a list of room members. * * @param string $roomid The room ID * @return Response */ public function get_room_members(string $roomid): Response { $params = [ ':roomid' => $roomid, ]; return $this->execute(new command( $this, method: 'GET', endpoint: '_matrix/client/v3/rooms/:roomid/joined_members', params: $params, )); } } spec/v1p6.php 0000604 00000002130 15062431010 0006763 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.6 of the Matrix specification. * * https://spec.matrix.org/v1.6/client-server-api/ * https://spec.matrix.org/v1.6/changelog/#api-changes * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p6 extends v1p5 { } spec/v1p1.php 0000604 00000003622 15062431010 0006765 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local\spec; /** * Matrix API to support version v1.1 of the Matrix specification. * * https://spec.matrix.org/v1.1/client-server-api/ * * @package communication_matrix * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class v1p1 extends \communication_matrix\matrix_client { // Use the standard matrix API for these features. use features\matrix\create_room_v3; use features\matrix\get_room_members_v3; use features\matrix\remove_member_from_room_v3; use features\matrix\update_room_avatar_v3; use features\matrix\update_room_name_v3; use features\matrix\update_room_topic_v3; use features\matrix\upload_content_v3; use features\matrix\update_room_power_levels_v3; use features\matrix\get_room_powerlevels_from_sync_v3; // We use the Synapse API here because it can invite users to a room without requiring them to accept the invite. use features\synapse\invite_member_to_room_v1; // User information and creation is a server-specific feature. use features\synapse\get_user_info_v2; use features\synapse\create_user_v2; use features\synapse\get_room_info_v1; } command.php 0000604 00000013464 15062431010 0006667 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace communication_matrix\local; use communication_matrix\matrix_client; use GuzzleHttp\Psr7\Request; use OutOfRangeException; /** * A command to be sent to the Matrix server. * * This class is a wrapper around the PSR-7 Request Interface implementation provided by Guzzle. * * It takes a set of common parameters and configurations and turns them into a Request that can be called against a live server. * * @package communication_matrix * @copyright Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class command extends Request { /** @var array $command The raw command data */ /** @var array|null $params The parameters passed into the command */ /** @var bool $sendasjson Whether to send params as JSON */ /** @var bool $requireauthorization Whether authorization is required for this request */ /** @var bool $ignorehttperrors Whether to ignore HTTP Errors */ /** @var array $query Any query parameters to set on the URL */ /** @var array|null Any parameters not used in the URI which are to be passed to the server via body or query params */ protected array $remainingparams = []; /** * Create a new Command. * * @param matrix_client $client The URL for this method * @param string $method (GET|POST|PUT|DELETE) * @param string $endpoint The URL * @param array $params Any parameters to pass * @param array $query Any query parameters to set on the URL * @param bool $ignorehttperrors Whether to ignore HTTP Errors * @param bool $requireauthorization Whether authorization is required for this request * @param bool $sendasjson Whether to send params as JSON */ public function __construct( protected matrix_client $client, string $method, string $endpoint, protected array $params = [], protected array $query = [], protected bool $ignorehttperrors = false, protected bool $requireauthorization = true, protected bool $sendasjson = true, ) { foreach ($params as $name => $value) { if ($name[0] === ':') { if (preg_match("/{$name}\\b/", $endpoint) !== 1) { throw new OutOfRangeException("Parameter not found in URL '{$name}'"); } $endpoint = preg_replace("/{$name}\\b/", urlencode($value), $endpoint); unset($params[$name]); } } // Store the modified params. $this->remainingparams = $params; if (str_contains($endpoint, '/:')) { throw new OutOfRangeException("URL contains untranslated parameters '{$endpoint}'"); } // Process the required headers. $headers = [ 'Content-Type' => 'application/json', ]; if ($this->require_authorization()) { $headers['Authorization'] = 'Bearer ' . $this->client->get_token(); } // Construct the final request. parent::__construct( $method, $this->get_url($endpoint), $headers, ); } /** * Get the URL of the endpoint on the server. * * @param string $endpoint * @return string */ protected function get_url(string $endpoint): string { return sprintf( "%s/%s", $this->client->get_server_url(), $endpoint, ); } /** * Get all parameters, including those set in the URL. * * @return array */ public function get_all_params(): array { return $this->params; } /** * Get the parameters provided to the command which are not used in the URL. * * These are typically passed to the server as query or body parameters instead. * * @return array */ public function get_remaining_params(): array { return $this->remainingparams; } /** * Get the Guzzle options to pass into the request. * * @return array */ public function get_options(): array { $options = []; if (count($this->query)) { $options['query'] = $this->query; } if ($this->should_send_params_as_json()) { $options['json'] = $this->get_remaining_params(); } if ($this->should_ignore_http_errors()) { $options['http_errors'] = false; } return $options; } /** * Whether authorization is required. * * Based on the 'authorization' attribute set in a raw command. * * @return bool */ public function require_authorization(): bool { return $this->requireauthorization; } /** * Whether to ignore http errors on the response. * * Based on the 'ignore_http_errors' attribute set in a raw command. * * @return bool */ public function should_ignore_http_errors(): bool { return $this->ignorehttperrors; } /** * Whether to send remaining parameters as JSON. * * @return bool */ public function should_send_params_as_json(): bool { return $this->sendasjson; } } callback/service_provider.php 0000604 00000005231 15062440633 0012363 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the \core_payment\local\local\callback\service_provider interface. * * Plugins should implement this if they use payment subsystem. * * @package core_payment * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_payment\local\callback; /** * The service_provider interface for plugins to provide callbacks which are needed by the payment subsystem. * * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface service_provider { /** * Callback function that returns the cost of the given item in the specified payment area, * along with the accountid that payments are paid to. * * @param string $paymentarea Payment area * @param int $itemid An identifier that is known to the plugin * @return \core_payment\local\entities\payable */ public static function get_payable(string $paymentarea, int $itemid): \core_payment\local\entities\payable; /** * Callback function that returns the URL of the page the user should be redirected to in the case of a successful payment. * * @param string $paymentarea Payment area * @param int $itemid An identifier that is known to the plugin * @return \moodle_url */ public static function get_success_url(string $paymentarea, int $itemid): \moodle_url; /** * Callback function that delivers what the user paid for to them. * * @param string $paymentarea Payment area * @param int $itemid An identifier that is known to the plugin * @param int $paymentid payment id as inserted into the 'payments' table, if needed for reference * @param int $userid The userid the order is going to deliver to * * @return bool Whether successful or not */ public static function deliver_order(string $paymentarea, int $itemid, int $paymentid, int $userid): bool; } entities/payable.php 0000604 00000003545 15062440633 0010524 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * The payable class. * * @package core_payment * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_payment\local\entities; /** * The payable class. * * @copyright 2020 Shamim Rezaie <shamim@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class payable { private $amount; private $currency; private $accountid; public function __construct(float $amount, string $currency, int $accountid) { $this->amount = $amount; $this->currency = $currency; $this->accountid = $accountid; } /** * Get the amount of the payable cost. * * @return float */ public function get_amount(): float { return $this->amount; } /** * Get the currency of the payable cost. * * @return string */ public function get_currency(): string { return $this->currency; } /** * Get the id of the payment account the cost is payable to. * * @return int */ public function get_account_id(): int { return $this->accountid; } } concept_cache.php 0000604 00000024002 15062454331 0010030 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Entry caching for glossary filter. * * @package mod_glossary * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_glossary\local; defined('MOODLE_INTERNAL') || die(); /** * Concept caching for glossary filter. * * @package mod_glossary * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class concept_cache { /** * Event observer, do not call directly. * @param \core\event\course_module_updated $event */ public static function cm_updated(\core\event\course_module_updated $event) { if ($event->other['modulename'] !== 'glossary') { return; } // We do not know what changed exactly, so let's reset everything that might be affected. concept_cache::reset_course_muc($event->courseid); concept_cache::reset_global_muc(); } /** * Reset concept related caches. * @param bool $phpunitreset */ public static function reset_caches($phpunitreset = false) { if ($phpunitreset) { return; } $cache = \cache::make('mod_glossary', 'concepts'); $cache->purge(); } /** * Reset the cache for course concepts. * @param int $courseid */ public static function reset_course_muc($courseid) { if (empty($courseid)) { return; } $cache = \cache::make('mod_glossary', 'concepts'); $cache->delete((int)$courseid); } /** * Reset the cache for global concepts. */ public static function reset_global_muc() { $cache = \cache::make('mod_glossary', 'concepts'); $cache->delete(0); } /** * Utility method to purge caches related to given glossary. * @param \stdClass $glossary */ public static function reset_glossary($glossary) { if (!$glossary->usedynalink) { return; } self::reset_course_muc($glossary->course); if ($glossary->globalglossary) { self::reset_global_muc(); } } /** * Fetch concepts for given glossaries. * @param int[] $glossaries * @return array */ protected static function fetch_concepts(array $glossaries) { global $DB; $glossarylist = implode(',', $glossaries); $sql = "SELECT id, glossaryid, concept, casesensitive, 0 AS category, fullmatch FROM {glossary_entries} WHERE glossaryid IN ($glossarylist) AND usedynalink = 1 AND approved = 1 UNION SELECT id, glossaryid, name AS concept, 1 AS casesensitive, 1 AS category, 1 AS fullmatch FROM {glossary_categories} WHERE glossaryid IN ($glossarylist) AND usedynalink = 1 UNION SELECT ge.id, ge.glossaryid, ga.alias AS concept, ge.casesensitive, 0 AS category, ge.fullmatch FROM {glossary_alias} ga JOIN {glossary_entries} ge ON (ga.entryid = ge.id) WHERE ge.glossaryid IN ($glossarylist) AND ge.usedynalink = 1 AND ge.approved = 1"; $concepts = array(); $rs = $DB->get_recordset_sql($sql); foreach ($rs as $concept) { $currentconcept = trim(strip_tags($concept->concept)); // Concept must be HTML-escaped, so do the same as format_string to turn ampersands into &. $currentconcept = replace_ampersands_not_followed_by_entity($currentconcept); if (empty($currentconcept)) { continue; } // Rule out any small integers, see MDL-1446. if (is_number($currentconcept) and $currentconcept < 1000) { continue; } $concept->concept = $currentconcept; $concepts[$concept->glossaryid][] = $concept; } $rs->close(); return $concepts; } /** * Get all linked concepts from course. * @param int $courseid * @return array */ protected static function get_course_concepts($courseid) { global $DB; if (empty($courseid)) { return array(array(), array()); } $courseid = (int)$courseid; // Get info on any glossaries in this course. $modinfo = get_fast_modinfo($courseid); $cminfos = $modinfo->get_instances_of('glossary'); if (!$cminfos) { // No glossaries in this course, so don't do any work. return array(array(), array()); } $cache = \cache::make('mod_glossary', 'concepts'); $data = $cache->get($courseid); if (is_array($data)) { list($glossaries, $allconcepts) = $data; } else { // Find all course glossaries. $sql = "SELECT g.id, g.name FROM {glossary} g JOIN {course_modules} cm ON (cm.instance = g.id) JOIN {modules} m ON (m.name = 'glossary' AND m.id = cm.module) WHERE g.usedynalink = 1 AND g.course = :course AND cm.visible = 1 AND m.visible = 1 ORDER BY g.globalglossary, g.id"; $glossaries = $DB->get_records_sql_menu($sql, array('course' => $courseid)); if (!$glossaries) { $data = array(array(), array()); $cache->set($courseid, $data); return $data; } foreach ($glossaries as $id => $name) { $name = str_replace(':', '-', $name); $glossaries[$id] = replace_ampersands_not_followed_by_entity(strip_tags($name)); } $allconcepts = self::fetch_concepts(array_keys($glossaries)); foreach ($glossaries as $gid => $unused) { if (!isset($allconcepts[$gid])) { unset($glossaries[$gid]); } } if (!$glossaries) { // This means there are no interesting concepts in the existing glossaries. $data = array(array(), array()); $cache->set($courseid, $data); return $data; } $cache->set($courseid, array($glossaries, $allconcepts)); } $concepts = $allconcepts; // Verify access control to glossary instances. foreach ($concepts as $modid => $unused) { if (!isset($cminfos[$modid])) { // This should not happen. unset($concepts[$modid]); unset($glossaries[$modid]); continue; } if (!$cminfos[$modid]->uservisible) { unset($concepts[$modid]); unset($glossaries[$modid]); continue; } } return array($glossaries, $concepts); } /** * Get all linked global concepts. * @return array */ protected static function get_global_concepts() { global $DB; $cache = \cache::make('mod_glossary', 'concepts'); $data = $cache->get(0); if (is_array($data)) { list($glossaries, $allconcepts) = $data; } else { // Find all global glossaries - no access control here. $sql = "SELECT g.id, g.name FROM {glossary} g JOIN {course_modules} cm ON (cm.instance = g.id) JOIN {modules} m ON (m.name = 'glossary' AND m.id = cm.module) WHERE g.usedynalink = 1 AND g.globalglossary = 1 AND cm.visible = 1 AND m.visible = 1 ORDER BY g.globalglossary, g.id"; $glossaries = $DB->get_records_sql_menu($sql); if (!$glossaries) { $data = array(array(), array()); $cache->set(0, $data); return $data; } foreach ($glossaries as $id => $name) { $name = str_replace(':', '-', $name); $glossaries[$id] = replace_ampersands_not_followed_by_entity(strip_tags($name)); } $allconcepts = self::fetch_concepts(array_keys($glossaries)); foreach ($glossaries as $gid => $unused) { if (!isset($allconcepts[$gid])) { unset($glossaries[$gid]); } } $cache->set(0, array($glossaries, $allconcepts)); } // NOTE: no access control is here because it would be way too expensive to check access // to all courses that contain the global glossaries. return array($glossaries, $allconcepts); } /** * Get all concepts that should be linked in the given course. * @param int $courseid * @return array with two elements - array of glossaries and concepts for each glossary */ public static function get_concepts($courseid) { list($glossaries, $concepts) = self::get_course_concepts($courseid); list($globalglossaries, $globalconcepts) = self::get_global_concepts(); foreach ($globalconcepts as $gid => $cs) { if (!isset($concepts[$gid])) { $concepts[$gid] = $cs; } } foreach ($globalglossaries as $gid => $name) { if (!isset($glossaries[$gid])) { $glossaries[$gid] = $name; } } return array($glossaries, $concepts); } } entities/enrolment.php 0000604 00000025123 15062456203 0011106 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use context_course; use core_course\reportbuilder\local\formatters\enrolment as enrolment_formatter; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use core_user\output\status_field; use enrol_plugin; use lang_string; use stdClass; /** * Course enrolment entity implementation * * @package core_course * @copyright 2022 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrolment extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['user_enrolments' => 'ue', 'enrol' => 'e']; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('enrolment', 'enrol'); } /** * Initialise the entity * * @return base */ public function initialise(): base { foreach ($this->get_all_columns() as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. foreach ($this->get_all_filters() as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $userenrolments = $this->get_table_alias('user_enrolments'); $enrol = $this->get_table_alias('enrol'); // Enrolment method column (Deprecated since Moodle 4.3, to remove in MDL-78118). $columns[] = (new column( 'method', new lang_string('method', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$enrol}.enrol, {$enrol}.id") ->set_is_sortable(true) ->set_is_deprecated('See \'enrol:name\' for replacement') ->add_callback([enrolment_formatter::class, 'enrolment_name']); // Enrolment time created. $columns[] = (new column( 'timecreated', new lang_string('timecreated', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$userenrolments}.timecreated") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Enrolment time started. $columns[] = (new column( 'timestarted', new lang_string('timestarted', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field(" CASE WHEN {$userenrolments}.timestart = 0 THEN {$userenrolments}.timecreated ELSE {$userenrolments}.timestart END", 'timestarted') ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Enrolment time ended. $columns[] = (new column( 'timeended', new lang_string('timeended', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$userenrolments}.timeend") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Enrolment status. $columns[] = (new column( 'status', new lang_string('status', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field($this->get_status_field_sql(), 'status') ->set_is_sortable(true) ->add_callback([enrolment_formatter::class, 'enrolment_status']); // Role column (Deprecated since Moodle 4.3, to remove in MDL-78118). $ctx = database::generate_alias(); $ra = database::generate_alias(); $r = database::generate_alias(); $columns[] = (new column( 'role', new lang_string('role', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("LEFT JOIN {context} {$ctx} ON {$ctx}.instanceid = {$enrol}.courseid AND {$ctx}.contextlevel = " . CONTEXT_COURSE) ->add_join("LEFT JOIN {role_assignments} {$ra} ON {$ra}.contextid = {$ctx}.id AND {$ra}.userid = {$userenrolments}.userid") ->add_join("LEFT JOIN {role} {$r} ON {$r}.id = {$ra}.roleid") ->set_type(column::TYPE_TEXT) ->add_fields("{$r}.id, {$r}.name, {$r}.shortname, {$ctx}.instanceid") ->set_is_sortable(true, ["{$r}.shortname"]) ->set_is_deprecated('See \'role:name\' for replacement') ->add_callback(static function(?string $value, stdClass $row): string { if (!$row->id) { return ''; } $context = context_course::instance($row->instanceid); return role_get_name($row, $context, ROLENAME_ALIAS); }); return $columns; } /** * Generate SQL snippet suitable for returning enrolment status field * * @return string */ private function get_status_field_sql(): string { $time = time(); $userenrolments = $this->get_table_alias('user_enrolments'); $enrol = $this->get_table_alias('enrol'); return " CASE WHEN {$userenrolments}.status = " . ENROL_USER_ACTIVE . " THEN CASE WHEN ({$userenrolments}.timestart > {$time}) OR ({$userenrolments}.timeend > 0 AND {$userenrolments}.timeend < {$time}) OR ({$enrol}.status = " . ENROL_INSTANCE_DISABLED . ") THEN " . status_field::STATUS_NOT_CURRENT . " ELSE " . status_field::STATUS_ACTIVE . " END ELSE {$userenrolments}.status END"; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $userenrolments = $this->get_table_alias('user_enrolments'); $enrol = $this->get_table_alias('enrol'); // Enrolment method (Deprecated since Moodle 4.3, to remove in MDL-78118). $enrolmentmethods = static function(): array { return array_map(static function(enrol_plugin $plugin): string { return get_string('pluginname', 'enrol_' . $plugin->get_name()); }, enrol_get_plugins(true)); }; $filters[] = (new filter( select::class, 'method', new lang_string('method', 'enrol'), $this->get_entity_name(), "{$enrol}.enrol" )) ->add_joins($this->get_joins()) ->set_is_deprecated('See \'enrol:plugin\' for replacement') ->set_options_callback($enrolmentmethods); // Enrolment time created. $filters[] = (new filter( date::class, 'timecreated', new lang_string('timecreated', 'moodle'), $this->get_entity_name(), "{$userenrolments}.timecreated" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Enrolment time started. $filters[] = (new filter( date::class, 'timestarted', new lang_string('timestarted', 'enrol'), $this->get_entity_name(), "CASE WHEN {$userenrolments}.timestart = 0 THEN {$userenrolments}.timecreated ELSE {$userenrolments}.timestart END" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Enrolment time ended. $filters[] = (new filter( date::class, 'timeended', new lang_string('timeended', 'enrol'), $this->get_entity_name(), "{$userenrolments}.timeend" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Enrolment status. $filters[] = (new filter( select::class, 'status', new lang_string('status', 'moodle'), $this->get_entity_name(), $this->get_status_field_sql() )) ->add_joins($this->get_joins()) ->set_options(enrolment_formatter::enrolment_values()); return $filters; } } entities/course_category.php 0000604 00000022264 15062456203 0012303 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use context_coursecat; use context_helper; use html_writer; use lang_string; use moodle_url; use stdClass; use core_course_category; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{category, text}; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; /** * Course category entity * * @package core_course * @copyright 2021 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_category extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'context' => 'ccctx', 'course_categories' => 'cc', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('coursecategory'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $tablealias = $this->get_table_alias('course_categories'); $tablealiascontext = $this->get_table_alias('context'); // Name column. $columns[] = (new column( 'name', new lang_string('categoryname'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->add_callback(static function(?string $name, stdClass $category): string { if (empty($category->id)) { return ''; } context_helper::preload_from_record($category); $context = context_coursecat::instance($category->id); return format_string($category->name, true, ['context' => $context]); }) ->set_is_sortable(true); // Category name with link column. $columns[] = (new column( 'namewithlink', new lang_string('namewithlink', 'core_course'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->add_callback(static function(?string $name, stdClass $category): string { if (empty($category->id)) { return ''; } context_helper::preload_from_record($category); $context = context_coursecat::instance($category->id); $url = new moodle_url('/course/management.php', ['categoryid' => $category->id]); return html_writer::link($url, format_string($category->name, true, ['context' => $context])); }) ->set_is_sortable(true); // Path column. $columns[] = (new column( 'path', new lang_string('categorypath'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.name, {$tablealias}.id") ->add_callback(static function(?string $name, stdClass $category): string { return empty($category->id) ? '' : core_course_category::get($category->id, MUST_EXIST, true)->get_nested_name(false); }) ->set_disabled_aggregation(['groupconcat', 'groupconcatdistinct']) ->set_is_sortable(true); // ID number column. $columns[] = (new column( 'idnumber', new lang_string('idnumbercoursecategory'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$tablealias}.idnumber") ->set_is_sortable(true); // Description column (note we need to join/select from the context table in order to format the column). $descriptionfieldsql = "{$tablealias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_fields("{$tablealias}.descriptionformat, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->add_callback(static function(?string $description, stdClass $category): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); if ($description === null) { return ''; } context_helper::preload_from_record($category); $context = context_coursecat::instance($category->id); $description = file_rewrite_pluginfile_urls($description, 'pluginfile.php', $context->id, 'coursecat', 'description', null); return format_text($description, $category->descriptionformat, ['context' => $context->id]); }); // Course count column. $columns[] = (new column( 'coursecount', new lang_string('coursecount', 'core_course'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_fields("{$tablealias}.coursecount") ->set_is_sortable(true); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('course_categories'); // Select category filter. $filters[] = (new filter( category::class, 'name', new lang_string('categoryselect', 'core_reportbuilder'), $this->get_entity_name(), "{$tablealias}.id" )) ->add_joins($this->get_joins()) ->set_options([ 'requiredcapabilities' => 'moodle/category:viewcourselist', ]); // Name filter. $filters[] = (new filter( text::class, 'text', new lang_string('categoryname'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()); // ID number filter. $filters[] = (new filter( text::class, 'idnumber', new lang_string('idnumbercoursecategory'), $this->get_entity_name(), "{$tablealias}.idnumber" )) ->add_joins($this->get_joins()); return $filters; } /** * Return context join used by columns * * @return string */ public function get_context_join(): string { $coursecategories = $this->get_table_alias('course_categories'); $context = $this->get_table_alias('context'); return "LEFT JOIN {context} {$context} ON {$context}.instanceid = {$coursecategories}.id AND {$context}.contextlevel = " . CONTEXT_COURSECAT; } } entities/completion.php 0000604 00000032356 15062456203 0011262 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use core_reportbuilder\local\entities\base; use core_course\reportbuilder\local\formatters\completion as completion_formatter; use core_reportbuilder\local\filters\boolean_select; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use completion_criteria_completion; use completion_info; use html_writer; use lang_string; use stdClass; /** * Course completion entity implementation * * @package core_course * @copyright 2022 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'course_completion' => 'ccomp', 'course' => 'c', 'grade_grades' => 'gg', 'grade_items' => 'gi', 'user' => 'u', ]; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('coursecompletion', 'completion'); } /** * Initialise the entity * * @return base */ public function initialise(): base { foreach ($this->get_all_columns() as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. foreach ($this->get_all_filters() as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $coursecompletion = $this->get_table_alias('course_completion'); $course = $this->get_table_alias('course'); $grade = $this->get_table_alias('grade_grades'); $gradeitem = $this->get_table_alias('grade_items'); $user = $this->get_table_alias('user'); // Completed column. $columns[] = (new column( 'completed', new lang_string('completed', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_field("CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END", 'completed') ->add_field("{$user}.id", 'userid') ->set_is_sortable(true) ->add_callback(static function(bool $value, stdClass $row): string { if (!$row->userid) { return ''; } return format::boolean_as_text($value); }); // Completion criteria column. $criterias = database::generate_alias(); $columns[] = (new column( 'criteria', new lang_string('criteria', 'core_completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) // Determine whether any criteria exist for the course. We also group per course, rather than report each separately. ->add_join("LEFT JOIN ( SELECT DISTINCT course FROM {course_completion_criteria} ) {$criterias} ON {$criterias}.course = {$course}.id") ->set_type(column::TYPE_TEXT) // Select enough fields to determine user criteria for the course. ->add_field("{$criterias}.course", 'courseid') ->add_field("{$course}.enablecompletion") ->add_field("{$user}.id", 'userid') ->set_disabled_aggregation_all() ->add_callback(static function($id, stdClass $record): string { if (!$record->courseid) { return ''; } $info = new completion_info((object) ['id' => $record->courseid, 'enablecompletion' => $record->enablecompletion]); if ($info->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) { $title = get_string('criteriarequiredall', 'core_completion'); } else { $title = get_string('criteriarequiredany', 'core_completion'); } // Map all completion data to their criteria summaries. $items = array_map(static function(completion_criteria_completion $completion): string { $criteria = $completion->get_criteria(); return get_string('criteriasummary', 'core_completion', [ 'type' => $criteria->get_details($completion)['type'], 'summary' => $criteria->get_title_detailed(), ]); }, $info->get_completions((int) $record->userid)); return $title . html_writer::alist($items); }); // Progress percentage column. $columns[] = (new column( 'progresspercent', new lang_string('progress', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$course}.id", 'courseid') ->add_field("{$user}.id", 'userid') ->set_is_sortable(false) ->add_callback([completion_formatter::class, 'completion_progress']); // Time enrolled. $columns[] = (new column( 'timeenrolled', new lang_string('timeenrolled', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.timeenrolled") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time started. $columns[] = (new column( 'timestarted', new lang_string('timestarted', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.timestarted") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time completed. $columns[] = (new column( 'timecompleted', new lang_string('timecompleted', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.timecompleted") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Time reaggregated. $columns[] = (new column( 'reaggregate', new lang_string('timereaggregated', 'enrol'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$coursecompletion}.reaggregate") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Days taking course (days since course start date until completion or until current date if not completed). $currenttime = time(); $columns[] = (new column( 'dayscourse', new lang_string('daystakingcourse', 'course'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("( CASE WHEN {$coursecompletion}.timecompleted > 0 THEN {$coursecompletion}.timecompleted ELSE {$currenttime} END - {$course}.startdate) / " . DAYSECS, 'dayscourse') ->add_field("{$user}.id", 'userid') ->set_is_sortable(true) ->add_callback([completion_formatter::class, 'get_days']); // Days since last completion (days since last enrolment date until completion or until current date if not completed). $columns[] = (new column( 'daysuntilcompletion', new lang_string('daysuntilcompletion', 'completion'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("( CASE WHEN {$coursecompletion}.timecompleted > 0 THEN {$coursecompletion}.timecompleted ELSE {$currenttime} END - {$coursecompletion}.timeenrolled) / " . DAYSECS, 'daysuntilcompletion') ->add_field("{$user}.id", 'userid') ->set_is_sortable(true) ->add_callback([completion_formatter::class, 'get_days']); // Student course grade. $columns[] = (new column( 'grade', new lang_string('gradenoun'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join(" LEFT JOIN {grade_items} {$gradeitem} ON ({$gradeitem}.itemtype = 'course' AND {$course}.id = {$gradeitem}.courseid) ") ->add_join(" LEFT JOIN {grade_grades} {$grade} ON ({$user}.id = {$grade}.userid AND {$gradeitem}.id = {$grade}.itemid) ") ->set_type(column::TYPE_FLOAT) ->add_fields("{$grade}.finalgrade") ->set_is_sortable(true) ->add_callback(function(?float $value): string { if ($value === null) { return ''; } return format_float($value, 2); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $coursecompletion = $this->get_table_alias('course_completion'); // Completed status filter. $filters[] = (new filter( boolean_select::class, 'completed', new lang_string('completed', 'completion'), $this->get_entity_name(), "CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END" )) ->add_joins($this->get_joins()); // Time completed filter. $filters[] = (new filter( date::class, 'timecompleted', new lang_string('timecompleted', 'completion'), $this->get_entity_name(), "{$coursecompletion}.timecompleted" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); // Time enrolled/started filter and condition. $fields = ['timeenrolled', 'timestarted']; foreach ($fields as $field) { $filters[] = (new filter( date::class, $field, new lang_string($field, 'enrol'), $this->get_entity_name(), "{$coursecompletion}.{$field}" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); } // Time reaggregated filter and condition. $filters[] = (new filter( date::class, 'reaggregate', new lang_string('timereaggregated', 'enrol'), $this->get_entity_name(), "{$coursecompletion}.reaggregate" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); return $filters; } } entities/access.php 0000604 00000010141 15062456203 0010336 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_course\reportbuilder\local\entities; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use lang_string; use stdClass; /** * Course access entity implementation * * @package core_course * @copyright 2022 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class access extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['user_lastaccess' => 'ula', 'user' => 'u']; } /** * The default title for this entity in the list of columns/conditions/filters in the report builder * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('courseaccess', 'course'); } /** * Initialise the entity * * @return base */ public function initialise(): base { foreach ($this->get_all_columns() as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. foreach ($this->get_all_filters() as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('user_lastaccess'); $user = $this->get_table_alias('user'); // Last course access column. $columns[] = (new column( 'timeaccess', new lang_string('lastcourseaccess', 'moodle'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$tablealias}.timeaccess") ->add_field("{$user}.id", 'userid') ->set_is_sortable(true) ->add_callback([format::class, 'userdate']) ->add_callback(static function(string $value, stdClass $row): string { if (!$row->userid) { return ''; } if ($value === '') { return get_string('never'); } return $value; }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('user_lastaccess'); // Last course access filter. $filters[] = (new filter( date::class, 'timeaccess', new lang_string('lastcourseaccess', 'moodle'), $this->get_entity_name(), "{$tablealias}.timeaccess" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_NOT_EMPTY, date::DATE_EMPTY, date::DATE_RANGE, date::DATE_LAST, date::DATE_CURRENT, ]); return $filters; } } formatters/enrolment.php 0000604 00000005215 15062456203 0011450 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_course\reportbuilder\local\formatters; use core_user\output\status_field; use lang_string; use stdClass; /** * Formatters for the course enrolment entity * * @package core_course * @copyright 2022 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrolment { /** * Return enrolment plugin instance name * * @param string|null $value * @param stdClass $row * @return string * * @deprecated since Moodle 4.3 - please do not use this function any more (to remove in MDL-78118) */ public static function enrolment_name(?string $value, stdClass $row): string { global $DB; if (empty($value)) { return ''; } $instance = $DB->get_record('enrol', ['id' => $row->id, 'enrol' => $row->enrol], '*', MUST_EXIST); $plugin = enrol_get_plugin($row->enrol); return $plugin ? $plugin->get_instance_name($instance) : '-'; } /** * Returns list of enrolment statuses * * @return lang_string[] */ public static function enrolment_values(): array { return [ status_field::STATUS_ACTIVE => new lang_string('participationactive', 'enrol'), status_field::STATUS_SUSPENDED => new lang_string('participationsuspended', 'enrol'), status_field::STATUS_NOT_CURRENT => new lang_string('participationnotcurrent', 'enrol'), ]; } /** * Return enrolment status for user * * @param string|null $value * @return string|null */ public static function enrolment_status(?string $value): ?string { if ($value === null) { return null; } $statusvalues = self::enrolment_values(); $value = (int) $value; if (!array_key_exists($value, $statusvalues)) { return null; } return (string) $statusvalues[$value]; } } formatters/completion.php 0000604 00000004713 15062456203 0011620 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_course\reportbuilder\local\formatters; use core_completion\progress; use core_reportbuilder\local\helpers\format; use stdClass; /** * Formatters for the course completion entity * * @package core_course * @copyright 2022 David Matamoros <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion { /** * Return completion progress as a percentage * * @param string|null $value * @param stdClass $row * @return string */ public static function completion_progress(?string $value, stdClass $row): string { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Do not show progress if there is no userid. if (!$row->userid) { return ''; } // Make sure courseid and userid have a value, specially userid because get_course_progress_percentage() defaults // to the current user if this is null and the result would be wrong. $courseid = (int) $row->courseid; $userid = (int) $row->userid; if ($courseid === 0 || $userid === 0) { return format::percent(0); } $course = get_course($courseid); $progress = (float) progress::get_course_progress_percentage($course, $userid); return format::percent($progress); } /** * Return number of days for methods daystakingcourse and daysuntilcompletion * * @param int|null $value * @param stdClass $row * @return int|null */ public static function get_days(?int $value, stdClass $row): ?int { // Do not show anything if there is no userid. if (!$row->userid) { return null; } return $value; } } activitychooser/dialogue.min.js.map 0000604 00000077675 15062475555 0013510 0 ustar 00 {"version":3,"file":"dialogue.min.js","sources":["../../../src/local/activitychooser/dialogue.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 <http://www.gnu.org/licenses/>.\n\n/**\n * A type of dialogue used as for choosing options.\n *\n * @module core_course/local/activitychooser/dialogue\n * @copyright 2019 Mihail Geshoski <mihail@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport * as ModalEvents from 'core/modal_events';\nimport selectors from 'core_course/local/activitychooser/selectors';\nimport * as Templates from 'core/templates';\nimport {end, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport {addIconToContainer} from 'core/loadingicon';\nimport * as Repository from 'core_course/local/activitychooser/repository';\nimport Notification from 'core/notification';\nimport {debounce} from 'core/utils';\nconst getPlugin = pluginName => import(pluginName);\n\n/**\n * Given an event from the main module 'page' navigate to it's help section via a carousel.\n *\n * @method showModuleHelp\n * @param {jQuery} carousel Our initialized carousel to manipulate\n * @param {Object} moduleData Data of the module to carousel to\n * @param {jQuery} modal We need to figure out if the current modal has a footer.\n */\nconst showModuleHelp = (carousel, moduleData, modal = null) => {\n // If we have a real footer then we need to change temporarily.\n if (modal !== null && moduleData.showFooter === true) {\n modal.setFooter(Templates.render('core_course/local/activitychooser/footer_partial', moduleData));\n }\n const help = carousel.find(selectors.regions.help)[0];\n help.innerHTML = '';\n help.classList.add('m-auto');\n\n // Add a spinner.\n const spinnerPromise = addIconToContainer(help);\n\n // Used later...\n let transitionPromiseResolver = null;\n const transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n // Build up the html & js ready to place into the help section.\n const contentPromise = Templates.renderForPromise('core_course/local/activitychooser/help', moduleData);\n\n // Wait for the content to be ready, and for the transition to be complet.\n Promise.all([contentPromise, spinnerPromise, transitionPromise])\n .then(([{html, js}]) => Templates.replaceNodeContents(help, html, js))\n .then(() => {\n help.querySelector(selectors.regions.chooserSummary.header).focus();\n return help;\n })\n .catch(Notification.exception);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.one('slid.bs.carousel', () => {\n transitionPromiseResolver();\n });\n // Trigger the transition between 'pages'.\n carousel.carousel('next');\n};\n\n/**\n * Given a user wants to change the favourite state of a module we either add or remove the status.\n * We also propergate this change across our map of modals.\n *\n * @method manageFavouriteState\n * @param {HTMLElement} modalBody The DOM node of the modal to manipulate\n * @param {HTMLElement} caller\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n */\nconst manageFavouriteState = async(modalBody, caller, partialFavourite) => {\n const isFavourite = caller.dataset.favourited;\n const id = caller.dataset.id;\n const name = caller.dataset.name;\n const internal = caller.dataset.internal;\n // Switch on fave or not.\n if (isFavourite === 'true') {\n await Repository.unfavouriteModule(name, id);\n\n partialFavourite(internal, false, modalBody);\n } else {\n await Repository.favouriteModule(name, id);\n\n partialFavourite(internal, true, modalBody);\n }\n\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nconst registerListenerEvents = (modal, mappedModules, partialFavourite, footerData) => {\n const bodyClickListener = async(e) => {\n if (e.target.closest(selectors.actions.optionActions.showSummary)) {\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n\n if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {\n const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);\n await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);\n const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = modal.getBody()[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions, modal);\n }\n\n // From the help screen go back to the module overview.\n if (e.target.matches(selectors.actions.closeOption)) {\n const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));\n\n // Trigger the transition between 'pages'.\n carousel.carousel('prev');\n carousel.on('slid.bs.carousel', () => {\n const allModules = modal.getBody()[0].querySelector(selectors.regions.modules);\n const caller = allModules.querySelector(selectors.regions.getModuleSelector(e.target.dataset.modname));\n caller.focus();\n });\n }\n\n // The \"clear search\" button is triggered.\n if (e.target.closest(selectors.actions.clearSearch)) {\n // Clear the entered search query in the search bar and hide the search results container.\n const searchInput = modal.getBody()[0].querySelector(selectors.actions.search);\n searchInput.value = \"\";\n searchInput.focus();\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }\n };\n\n // We essentially have two types of footer.\n // A fake one that is handled within the template for chooser_help and then all of the stuff for\n // modal.footer. We need to ensure we know exactly what type of footer we are using so we know what we\n // need to manage. The below code handles a real footer going to a mnet carousel item.\n const footerClickListener = async(e) => {\n if (footerData.footer === true) {\n const footerjs = await getPlugin(footerData.customfooterjs);\n await footerjs.footerClickListener(e, footerData, modal);\n }\n };\n\n modal.getBodyPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n\n // Set up the carousel.\n .then(body => {\n $(body.querySelector(selectors.regions.carousel))\n .carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n return body;\n })\n\n // Add the listener for clicks on the body.\n .then(body => {\n body.addEventListener('click', bodyClickListener);\n return body;\n })\n\n // Add a listener for an input change in the activity chooser's search bar.\n .then(body => {\n const searchInput = body.querySelector(selectors.actions.search);\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n toggleSearchResultsView(modal, mappedModules, searchInput.value);\n }, 300));\n return body;\n })\n\n // Register event listeners related to the keyboard navigation controls.\n .then(body => {\n // Get the active chooser options section.\n const activeSectionId = body.querySelector(selectors.elements.activetab).getAttribute(\"href\");\n const sectionChooserOptions = body.querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = sectionChooserOptions.querySelector(selectors.regions.chooserOption.container);\n\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body, mappedModules, sectionChooserOptions, modal);\n\n return body;\n })\n .catch();\n\n modal.getFooterPromise()\n\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(footer => footer[0])\n // Add the listener for clicks on the footer.\n .then(footer => {\n footer.addEventListener('click', footerClickListener);\n return footer;\n })\n .catch();\n};\n\n/**\n * Initialise the keyboard navigation controls for the chooser options.\n *\n * @method initChooserOptionsKeyboardNavigation\n * @param {HTMLElement} body Our modal that we are working with\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {HTMLElement} chooserOptionsContainer The section that contains the chooser items\n * @param {Object} modal Our created modal for the section\n */\nconst initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOptionsContainer, modal = null) => {\n const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);\n\n Array.from(chooserOptions).forEach((element) => {\n return element.addEventListener('keydown', (e) => {\n\n // Check for enter/ space triggers for showing the help.\n if (e.keyCode === enter || e.keyCode === space) {\n if (e.target.matches(selectors.actions.optionActions.showSummary)) {\n e.preventDefault();\n const module = e.target.closest(selectors.regions.chooserOption.container);\n const moduleName = module.dataset.modname;\n const moduleData = mappedModules.get(moduleName);\n const carousel = $(body.querySelector(selectors.regions.carousel));\n carousel.carousel({\n interval: false,\n pause: true,\n keyboard: false\n });\n\n // We need to know if the overall modal has a footer so we know when to show a real / vs fake footer.\n moduleData.showFooter = modal.hasFooterContent();\n showModuleHelp(carousel, moduleData, modal);\n }\n }\n\n // Next.\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const nextOption = currentOption.nextElementSibling;\n const firstOption = chooserOptionsContainer.firstElementChild;\n const toFocusOption = clickErrorHandler(nextOption, firstOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n // Previous.\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const previousOption = currentOption.previousElementSibling;\n const lastOption = chooserOptionsContainer.lastElementChild;\n const toFocusOption = clickErrorHandler(previousOption, lastOption);\n focusChooserOption(toFocusOption, currentOption);\n }\n\n if (e.keyCode === home) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const firstOption = chooserOptionsContainer.firstElementChild;\n focusChooserOption(firstOption, currentOption);\n }\n\n if (e.keyCode === end) {\n e.preventDefault();\n const currentOption = e.target.closest(selectors.regions.chooserOption.container);\n const lastOption = chooserOptionsContainer.lastElementChild;\n focusChooserOption(lastOption, currentOption);\n }\n });\n });\n};\n\n/**\n * Focus on a chooser option element and remove the previous chooser element from the focus order\n *\n * @method focusChooserOption\n * @param {HTMLElement} currentChooserOption The current chooser option element that we want to focus\n * @param {HTMLElement|null} previousChooserOption The previous focused option element\n */\nconst focusChooserOption = (currentChooserOption, previousChooserOption = null) => {\n if (previousChooserOption !== null) {\n toggleFocusableChooserOption(previousChooserOption, false);\n }\n\n toggleFocusableChooserOption(currentChooserOption, true);\n currentChooserOption.focus();\n};\n\n/**\n * Add or remove a chooser option from the focus order.\n *\n * @method toggleFocusableChooserOption\n * @param {HTMLElement} chooserOption The chooser option element which should be added or removed from the focus order\n * @param {Boolean} isFocusable Whether the chooser element is focusable or not\n */\nconst toggleFocusableChooserOption = (chooserOption, isFocusable) => {\n const chooserOptionLink = chooserOption.querySelector(selectors.actions.addChooser);\n const chooserOptionHelp = chooserOption.querySelector(selectors.actions.optionActions.showSummary);\n const chooserOptionFavourite = chooserOption.querySelector(selectors.actions.optionActions.manageFavourite);\n\n if (isFocusable) {\n // Set tabindex to 0 to add current chooser option element to the focus order.\n chooserOption.tabIndex = 0;\n chooserOptionLink.tabIndex = 0;\n chooserOptionHelp.tabIndex = 0;\n chooserOptionFavourite.tabIndex = 0;\n } else {\n // Set tabindex to -1 to remove the previous chooser option element from the focus order.\n chooserOption.tabIndex = -1;\n chooserOptionLink.tabIndex = -1;\n chooserOptionHelp.tabIndex = -1;\n chooserOptionFavourite.tabIndex = -1;\n }\n};\n\n/**\n * Small error handling function to make sure the navigated to object exists\n *\n * @method clickErrorHandler\n * @param {HTMLElement} item What we want to check exists\n * @param {HTMLElement} fallback If we dont match anything fallback the focus\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Render the search results in a defined container\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The container where the data should be rendered\n * @param {Object} searchResultsData Data containing the module items that satisfy the search criteria\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresultsnumber': searchResultsData.length,\n 'searchresults': searchResultsData\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_course/local/activitychooser/search_results', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * Toggle (display/hide) the search results depending on the value of the search query\n *\n * @method toggleSearchResultsView\n * @param {Object} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}\n * @param {String} searchQuery The search query\n */\nconst toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {\n const modalBody = modal.getBody()[0];\n const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);\n const chooserContainer = modalBody.querySelector(selectors.regions.chooser);\n const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);\n\n if (searchQuery.length > 0) { // Search query is present.\n const searchResultsData = searchModules(mappedModules, searchQuery);\n await renderSearchResults(searchResultsContainer, searchResultsData);\n const searchResultItemsContainer = searchResultsContainer.querySelector(selectors.regions.searchResultItems);\n const firstSearchResultItem = searchResultItemsContainer.querySelector(selectors.regions.chooserOption.container);\n if (firstSearchResultItem) {\n // Set the first result item to be focusable.\n toggleFocusableChooserOption(firstSearchResultItem, true);\n // Register keyboard events on the created search result items.\n initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);\n }\n // Display the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.remove('d-none');\n // Hide the default chooser options container.\n chooserContainer.setAttribute('hidden', 'hidden');\n // Display the search results container.\n searchResultsContainer.removeAttribute('hidden');\n } else { // Search query is not present.\n // Hide the \"clear\" search button in the activity chooser search bar.\n clearSearchButton.classList.add('d-none');\n // Hide the search results container.\n searchResultsContainer.setAttribute('hidden', 'hidden');\n // Display the default chooser options container.\n chooserContainer.removeAttribute('hidden');\n }\n};\n\n/**\n * Return the list of modules which have a name or description that matches the given search term.\n *\n * @method searchModules\n * @param {Array} modules List of available modules\n * @param {String} searchTerm The search term to match\n * @return {Array}\n */\nconst searchModules = (modules, searchTerm) => {\n if (searchTerm === '') {\n return modules;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n modules.forEach((activity) => {\n const activityName = activity.title.toLowerCase();\n const activityDesc = activity.help.toLowerCase();\n if (activityName.includes(searchTerm) || activityDesc.includes(searchTerm)) {\n searchResults.push(activity);\n }\n });\n\n return searchResults;\n};\n\n/**\n * Set up our tabindex information across the chooser.\n *\n * @method setupKeyboardAccessibility\n * @param {Promise} modal Our created modal for the section\n * @param {Map} mappedModules A map of all of the built module information\n */\nconst setupKeyboardAccessibility = (modal, mappedModules) => {\n modal.getModal()[0].tabIndex = -1;\n\n modal.getBodyPromise().then(body => {\n $(selectors.elements.tab).on('shown.bs.tab', (e) => {\n const activeSectionId = e.target.getAttribute(\"href\");\n const activeSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));\n const firstChooserOption = activeSectionChooserOptions\n .querySelector(selectors.regions.chooserOption.container);\n const prevActiveSectionId = e.relatedTarget.getAttribute(\"href\");\n const prevActiveSectionChooserOptions = body[0]\n .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));\n\n // Disable the focus of every chooser option in the previous active section.\n disableFocusAllChooserOptions(prevActiveSectionChooserOptions);\n // Enable the focus of the first chooser option in the current active section.\n toggleFocusableChooserOption(firstChooserOption, true);\n initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions, modal);\n });\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Disable the focus of all chooser options in a specific container (section).\n *\n * @method disableFocusAllChooserOptions\n * @param {HTMLElement} sectionChooserOptions The section that contains the chooser items\n */\nconst disableFocusAllChooserOptions = (sectionChooserOptions) => {\n const allChooserOptions = sectionChooserOptions.querySelectorAll(selectors.regions.chooserOption.container);\n allChooserOptions.forEach((chooserOption) => {\n toggleFocusableChooserOption(chooserOption, false);\n });\n};\n\n/**\n * Display the module chooser.\n *\n * @method displayChooser\n * @param {Promise} modalPromise Our created modal for the section\n * @param {Array} sectionModules An array of all of the built module information\n * @param {Function} partialFavourite Partially applied function we need to manage favourite status\n * @param {Object} footerData Our base footer object.\n */\nexport const displayChooser = (modalPromise, sectionModules, partialFavourite, footerData) => {\n // Make a map so we can quickly fetch a specific module's object for either rendering or searching.\n const mappedModules = new Map();\n sectionModules.forEach((module) => {\n mappedModules.set(module.componentname + '_' + module.link, module);\n });\n\n // Register event listeners.\n modalPromise.then(modal => {\n registerListenerEvents(modal, mappedModules, partialFavourite, footerData);\n\n // We want to focus on the first chooser option element as soon as the modal is opened.\n setupKeyboardAccessibility(modal, mappedModules);\n\n // We want to focus on the action select when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n\n return modal;\n }).catch();\n};\n"],"names":["showModuleHelp","carousel","moduleData","modal","showFooter","setFooter","Templates","render","help","find","selectors","regions","innerHTML","classList","add","spinnerPromise","transitionPromiseResolver","transitionPromise","Promise","resolve","contentPromise","renderForPromise","all","then","_ref","html","js","replaceNodeContents","querySelector","chooserSummary","header","focus","catch","Notification","exception","one","registerListenerEvents","mappedModules","partialFavourite","footerData","bodyClickListener","async","e","target","closest","actions","optionActions","showSummary","getBody","moduleName","chooserOption","container","dataset","modname","get","hasFooterContent","manageFavourite","caller","modalBody","isFavourite","favourited","id","name","internal","Repository","unfavouriteModule","favouriteModule","manageFavouriteState","activeSectionId","elements","activetab","getAttribute","sectionChooserOptions","getSectionChooserOptions","firstChooserOption","toggleFocusableChooserOption","initChooserOptionsKeyboardNavigation","matches","closeOption","on","modules","getModuleSelector","clearSearch","searchInput","search","value","toggleSearchResultsView","footerClickListener","footer","footerjs","pluginName","customfooterjs","getBodyPromise","body","interval","pause","keyboard","addEventListener","getFooterPromise","chooserOptionsContainer","chooserOptions","querySelectorAll","Array","from","forEach","element","keyCode","enter","space","preventDefault","arrowRight","currentOption","nextOption","nextElementSibling","firstOption","firstElementChild","toFocusOption","clickErrorHandler","focusChooserOption","arrowLeft","previousOption","previousElementSibling","lastOption","lastElementChild","home","end","currentChooserOption","previousChooserOption","isFocusable","chooserOptionLink","addChooser","chooserOptionHelp","chooserOptionFavourite","tabIndex","item","fallback","searchQuery","searchResultsContainer","searchResults","chooserContainer","chooser","clearSearchButton","length","searchResultsData","searchModules","templateData","renderSearchResults","searchResultItemsContainer","searchResultItems","firstSearchResultItem","remove","setAttribute","removeAttribute","searchTerm","toLowerCase","activity","activityName","title","activityDesc","includes","push","disableFocusAllChooserOptions","modalPromise","sectionModules","Map","module","set","componentname","link","getModal","tab","activeSectionChooserOptions","prevActiveSectionId","relatedTarget","prevActiveSectionChooserOptions","setupKeyboardAccessibility","getRoot","ModalEvents","hidden","destroy"],"mappings":"o5DA0CMA,eAAiB,SAACC,SAAUC,gBAAYC,6DAAQ,KAEpC,OAAVA,QAA4C,IAA1BD,WAAWE,YAC7BD,MAAME,UAAUC,UAAUC,OAAO,mDAAoDL,mBAEnFM,KAAOP,SAASQ,KAAKC,mBAAUC,QAAQH,MAAM,GACnDA,KAAKI,UAAY,GACjBJ,KAAKK,UAAUC,IAAI,gBAGbC,gBAAiB,mCAAmBP,UAGtCQ,0BAA4B,WAC1BC,kBAAoB,IAAIC,SAAQC,UAClCH,0BAA4BG,WAI1BC,eAAiBd,UAAUe,iBAAiB,yCAA0CnB,YAG5FgB,QAAQI,IAAI,CAACF,eAAgBL,eAAgBE,oBACxCM,MAAKC,YAAEC,KAACA,KAADC,GAAOA,iBAASpB,UAAUqB,oBAAoBnB,KAAMiB,KAAMC,OACjEH,MAAK,KACFf,KAAKoB,cAAclB,mBAAUC,QAAQkB,eAAeC,QAAQC,QACrDvB,QAEVwB,MAAMC,sBAAaC,WAGxBjC,SAASkC,IAAI,oBAAoB,KAC7BnB,+BAGJf,SAASA,SAAS,SAuChBmC,uBAAyB,CAACjC,MAAOkC,cAAeC,iBAAkBC,oBAC9DC,kBAAoBC,MAAAA,OAClBC,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQC,cAAcC,aAAc,OACzD9C,UAAW,mBAAEE,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUC,QAAQV,WAGhEgD,WADSP,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACtCC,QAAQC,QAC5BnD,WAAamC,cAAciB,IAAIL,YAErC/C,WAAWE,WAAaD,MAAMoD,mBAC9BvD,eAAeC,SAAUC,WAAYC,UAGrCuC,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQC,cAAcU,iBAAkB,OAC7DC,OAASf,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQC,cAAcU,sBAzC/Cf,OAAMiB,UAAWD,OAAQnB,0BAC5CqB,YAAcF,OAAOL,QAAQQ,WAC7BC,GAAKJ,OAAOL,QAAQS,GACpBC,KAAOL,OAAOL,QAAQU,KACtBC,SAAWN,OAAOL,QAAQW,SAEZ,SAAhBJ,mBACMK,WAAWC,kBAAkBH,KAAMD,IAEzCvB,iBAAiByB,UAAU,EAAOL,mBAE5BM,WAAWE,gBAAgBJ,KAAMD,IAEvCvB,iBAAiByB,UAAU,EAAML,aA6BvBS,CAAqBhE,MAAM6C,UAAU,GAAIS,OAAQnB,wBACjD8B,gBAAkBjE,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAU2D,SAASC,WAAWC,aAAa,QAC9FC,sBAAwBrE,MAAM6C,UAAU,GACzCpB,cAAclB,mBAAUC,QAAQ8D,yBAAyBL,kBACxDM,mBAAqBF,sBACtB5C,cAAclB,mBAAUC,QAAQuC,cAAcC,WACnDwB,6BAA6BD,oBAAoB,GACjDE,qCAAqCzE,MAAM6C,UAAU,GAAIX,cAAemC,sBAAuBrE,UAI/FuC,EAAEC,OAAOkC,QAAQnE,mBAAUmC,QAAQiC,aAAc,OAC3C7E,UAAW,mBAAEE,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUC,QAAQV,WAGtEA,SAASA,SAAS,QAClBA,SAAS8E,GAAG,oBAAoB,KACT5E,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUC,QAAQqE,SAC5CpD,cAAclB,mBAAUC,QAAQsE,kBAAkBvC,EAAEC,OAAOS,QAAQC,UACtFtB,cAKXW,EAAEC,OAAOC,QAAQlC,mBAAUmC,QAAQqC,aAAc,OAE3CC,YAAchF,MAAM6C,UAAU,GAAGpB,cAAclB,mBAAUmC,QAAQuC,QACvED,YAAYE,MAAQ,GACpBF,YAAYpD,QACZuD,wBAAwBnF,MAAOkC,cAAe8C,YAAYE,SAQ5DE,oBAAsB9C,MAAAA,QACE,IAAtBF,WAAWiD,OAAiB,OACtBC,eA1IAC,WA0I2BnD,WAAWoD,+NA1IjBD,4WAAAA,oBA2IrBD,SAASF,oBAAoB7C,EAAGH,WAAYpC,OA3I5CuF,IAAAA,YA+IdvF,MAAMyF,iBAGLrE,MAAKsE,MAAQA,KAAK,KAGlBtE,MAAKsE,2BACAA,KAAKjE,cAAclB,mBAAUC,QAAQV,WAClCA,SAAS,CACN6F,UAAU,EACVC,OAAO,EACPC,UAAU,IAGXH,QAIVtE,MAAKsE,OACFA,KAAKI,iBAAiB,QAASzD,mBACxBqD,QAIVtE,MAAKsE,aACIV,YAAcU,KAAKjE,cAAclB,mBAAUmC,QAAQuC,eAEzDD,YAAYc,iBAAiB,SAAS,oBAAS,KAE3CX,wBAAwBnF,MAAOkC,cAAe8C,YAAYE,SAC3D,MACIQ,QAIVtE,MAAKsE,aAEIzB,gBAAkByB,KAAKjE,cAAclB,mBAAU2D,SAASC,WAAWC,aAAa,QAChFC,sBAAwBqB,KAAKjE,cAAclB,mBAAUC,QAAQ8D,yBAAyBL,kBACtFM,mBAAqBF,sBAAsB5C,cAAclB,mBAAUC,QAAQuC,cAAcC,kBAE/FwB,6BAA6BD,oBAAoB,GACjDE,qCAAqCiB,KAAMxD,cAAemC,sBAAuBrE,OAE1E0F,QAEV7D,QAED7B,MAAM+F,mBAGL3E,MAAKiE,QAAUA,OAAO,KAEtBjE,MAAKiE,SACFA,OAAOS,iBAAiB,QAASV,qBAC1BC,UAEVxD,SAYC4C,qCAAuC,SAACiB,KAAMxD,cAAe8D,6BAAyBhG,6DAAQ,WAC1FiG,eAAiBD,wBAAwBE,iBAAiB3F,mBAAUC,QAAQuC,cAAcC,WAEhGmD,MAAMC,KAAKH,gBAAgBI,SAASC,SACzBA,QAAQR,iBAAiB,WAAYvD,QAGpCA,EAAEgE,UAAYC,kBAASjE,EAAEgE,UAAYE,mBACjClE,EAAEC,OAAOkC,QAAQnE,mBAAUmC,QAAQC,cAAcC,aAAc,CAC/DL,EAAEmE,uBAEI5D,WADSP,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACtCC,QAAQC,QAC5BnD,WAAamC,cAAciB,IAAIL,YAC/BhD,UAAW,mBAAE4F,KAAKjE,cAAclB,mBAAUC,QAAQV,WACxDA,SAASA,SAAS,CACd6F,UAAU,EACVC,OAAO,EACPC,UAAU,IAId9F,WAAWE,WAAaD,MAAMoD,mBAC9BvD,eAAeC,SAAUC,WAAYC,UAKzCuC,EAAEgE,UAAYI,sBAAY,CAC1BpE,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjE6D,WAAaD,cAAcE,mBAC3BC,YAAcf,wBAAwBgB,kBACtCC,cAAgBC,kBAAkBL,WAAYE,aACpDI,mBAAmBF,cAAeL,kBAIlCrE,EAAEgE,UAAYa,qBAAW,CACzB7E,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjEqE,eAAiBT,cAAcU,uBAC/BC,WAAavB,wBAAwBwB,iBACrCP,cAAgBC,kBAAkBG,eAAgBE,YACxDJ,mBAAmBF,cAAeL,kBAGlCrE,EAAEgE,UAAYkB,gBAAM,CACpBlF,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjE+D,YAAcf,wBAAwBgB,kBAC5CG,mBAAmBJ,YAAaH,kBAGhCrE,EAAEgE,UAAYmB,eAAK,CACnBnF,EAAEmE,uBACIE,cAAgBrE,EAAEC,OAAOC,QAAQlC,mBAAUC,QAAQuC,cAAcC,WACjEuE,WAAavB,wBAAwBwB,iBAC3CL,mBAAmBI,WAAYX,sBAazCO,mBAAqB,SAACQ,0BAAsBC,6EAAwB,KACxC,OAA1BA,uBACApD,6BAA6BoD,uBAAuB,GAGxDpD,6BAA6BmD,sBAAsB,GACnDA,qBAAqB/F,SAUnB4C,6BAA+B,CAACzB,cAAe8E,qBAC3CC,kBAAoB/E,cAActB,cAAclB,mBAAUmC,QAAQqF,YAClEC,kBAAoBjF,cAActB,cAAclB,mBAAUmC,QAAQC,cAAcC,aAChFqF,uBAAyBlF,cAActB,cAAclB,mBAAUmC,QAAQC,cAAcU,iBAEvFwE,aAEA9E,cAAcmF,SAAW,EACzBJ,kBAAkBI,SAAW,EAC7BF,kBAAkBE,SAAW,EAC7BD,uBAAuBC,SAAW,IAGlCnF,cAAcmF,UAAY,EAC1BJ,kBAAkBI,UAAY,EAC9BF,kBAAkBE,UAAY,EAC9BD,uBAAuBC,UAAY,IAYrChB,kBAAoB,CAACiB,KAAMC,WAChB,OAATD,KACOA,KAEAC,SA6BTjD,wBAA0B7C,MAAMtC,MAAOkC,cAAemG,qBAClD9E,UAAYvD,MAAM6C,UAAU,GAC5ByF,uBAAyB/E,UAAU9B,cAAclB,mBAAUC,QAAQ+H,eACnEC,iBAAmBjF,UAAU9B,cAAclB,mBAAUC,QAAQiI,SAC7DC,kBAAoBnF,UAAU9B,cAAclB,mBAAUmC,QAAQqC,gBAEhEsD,YAAYM,OAAS,EAAG,OAClBC,kBAAoBC,cAAc3G,cAAemG,kBAzBnC/F,OAAMgG,uBAAwBM,2BAChDE,aAAe,qBACMF,kBAAkBD,qBACxBC,oBAGftH,KAACA,KAADC,GAAOA,UAAYpB,UAAUe,iBAAiB,mDAAoD4H,oBAClG3I,UAAUqB,oBAAoB8G,uBAAwBhH,KAAMC,KAmBxDwH,CAAoBT,uBAAwBM,yBAC5CI,2BAA6BV,uBAAuB7G,cAAclB,mBAAUC,QAAQyI,mBACpFC,sBAAwBF,2BAA2BvH,cAAclB,mBAAUC,QAAQuC,cAAcC,WACnGkG,wBAEA1E,6BAA6B0E,uBAAuB,GAEpDzE,qCAAqClB,UAAWrB,cAAe8G,2BAA4BhJ,QAG/F0I,kBAAkBhI,UAAUyI,OAAO,UAEnCX,iBAAiBY,aAAa,SAAU,UAExCd,uBAAuBe,gBAAgB,eAGvCX,kBAAkBhI,UAAUC,IAAI,UAEhC2H,uBAAuBc,aAAa,SAAU,UAE9CZ,iBAAiBa,gBAAgB,WAYnCR,cAAgB,CAAChE,QAASyE,iBACT,KAAfA,kBACOzE,QAEXyE,WAAaA,WAAWC,oBAClBhB,cAAgB,UACtB1D,QAAQwB,SAASmD,iBACPC,aAAeD,SAASE,MAAMH,cAC9BI,aAAeH,SAASnJ,KAAKkJ,eAC/BE,aAAaG,SAASN,aAAeK,aAAaC,SAASN,cAC3Df,cAAcsB,KAAKL,aAIpBjB,eAwCLuB,8BAAiCzF,wBACTA,sBAAsB6B,iBAAiB3F,mBAAUC,QAAQuC,cAAcC,WAC/EqD,SAAStD,gBACvByB,6BAA6BzB,eAAe,+BAatB,CAACgH,aAAcC,eAAgB7H,iBAAkBC,oBAErEF,cAAgB,IAAI+H,IAC1BD,eAAe3D,SAAS6D,SACpBhI,cAAciI,IAAID,OAAOE,cAAgB,IAAMF,OAAOG,KAAMH,WAIhEH,aAAa3I,MAAKpB,QACdiC,uBAAuBjC,MAAOkC,cAAeC,iBAAkBC,YAvDpC,EAACpC,MAAOkC,iBACvClC,MAAMsK,WAAW,GAAGpC,UAAY,EAEhClI,MAAMyF,iBAAiBrE,MAAKsE,2BACtBnF,mBAAU2D,SAASqG,KAAK3F,GAAG,gBAAiBrC,UACpC0B,gBAAkB1B,EAAEC,OAAO4B,aAAa,QACxCoG,4BAA8B9E,KAAK,GACpCjE,cAAclB,mBAAUC,QAAQ8D,yBAAyBL,kBACxDM,mBAAqBiG,4BACtB/I,cAAclB,mBAAUC,QAAQuC,cAAcC,WAC7CyH,oBAAsBlI,EAAEmI,cAActG,aAAa,QACnDuG,gCAAkCjF,KAAK,GACxCjE,cAAclB,mBAAUC,QAAQ8D,yBAAyBmG,sBAG9DX,8BAA8Ba,iCAE9BnG,6BAA6BD,oBAAoB,GACjDE,qCAAqCiB,KAAK,GAAIxD,cAAesI,4BAA6BxK,aAG/F6B,MAAMC,sBAAaC,YAqClB6I,CAA2B5K,MAAOkC,eAGlClC,MAAM6K,UAAUjG,GAAGkG,YAAYC,QAAQ,KACnC/K,MAAMgL,aAGHhL,SACR6B"} activitychooser/selectors.min.js 0000604 00000005461 15062475555 0013126 0 ustar 00 define("core_course/local/activitychooser/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** * Define all of the selectors we will be using on the grading interface. * * @module core_course/local/activitychooser/selectors * @copyright 2019 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const getDataSelector=(name,value)=>"[data-".concat(name,'="').concat(value,'"]');var _default={regions:{chooser:getDataSelector("region","chooser-container"),getSectionChooserOptions:containerid=>"".concat(containerid," ").concat(getDataSelector("region","chooser-options-container")),chooserOption:{container:getDataSelector("region","chooser-option-container"),actions:getDataSelector("region","chooser-option-actions-container"),info:getDataSelector("region","chooser-option-info-container")},chooserSummary:{container:getDataSelector("region","chooser-option-summary-container"),content:getDataSelector("region","chooser-option-summary-content-container"),header:getDataSelector("region","summary-header"),actions:getDataSelector("region","chooser-option-summary-actions-container")},carousel:getDataSelector("region","carousel"),help:getDataSelector("region","help"),modules:getDataSelector("region","modules"),favouriteTabNav:getDataSelector("region","favourite-tab-nav"),defaultTabNav:getDataSelector("region","default-tab-nav"),activityTabNav:getDataSelector("region","activity-tab-nav"),favouriteTab:getDataSelector("region","favourites"),recommendedTab:getDataSelector("region","recommended"),defaultTab:getDataSelector("region","default"),activityTab:getDataSelector("region","activity"),resourceTab:getDataSelector("region","resources"),getModuleSelector:modname=>'[role="menuitem"][data-modname="'.concat(modname,'"]'),searchResults:getDataSelector("region","search-results-container"),searchResultItems:getDataSelector("region","search-result-items-container")},actions:{optionActions:{showSummary:getDataSelector("action","show-option-summary"),manageFavourite:getDataSelector("action","manage-module-favourite")},addChooser:getDataSelector("action","add-chooser-option"),closeOption:getDataSelector("action","close-chooser-option-summary"),hide:getDataSelector("action","hide"),search:getDataSelector("action","search"),clearSearch:getDataSelector("action","clearsearch")},render:{favourites:getDataSelector("render","favourites-area")},elements:{section:".section",sectionmodchooser:"button.section-modchooser-link",sitemenu:".block_site_main_menu",sitetopic:"div.sitetopic",tab:'a[data-toggle="tab"]',activetab:'a[data-toggle="tab"][aria-selected="true"]',visibletabs:'a[data-toggle="tab"]:not(.d-none)'}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=selectors.min.js.map activitychooser/repository.min.js.map 0000604 00000007751 15062475555 0014122 0 ustar 00 {"version":3,"file":"repository.min.js","sources":["../../../src/local/activitychooser/repository.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 <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle user AJAX actions.\n *\n * @module core_course/local/activitychooser/repository\n * @copyright 2019 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method activityModules\n * @param {Number} courseid What course to fetch the modules for\n * @return {object} jQuery promise\n */\nexport const activityModules = (courseid) => {\n const request = {\n methodname: 'core_course_get_course_content_items',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is a users' favourite.\n *\n * @method favouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const favouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_add_content_item_to_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a module name, module ID & the current course we want to specify that the module\n * is no longer a users' favourite.\n *\n * @method unfavouriteModule\n * @param {String} modName Frankenstyle name of the component to add favourite\n * @param {int} modID ID of the module. Mainly for LTI cases where they have same / similar names\n * @return {object} jQuery promise\n */\nexport const unfavouriteModule = (modName, modID) => {\n const request = {\n methodname: 'core_course_remove_content_item_from_user_favourites',\n args: {\n componentname: modName,\n contentitemid: modID,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Fetch all the information on modules we'll need in the activity chooser.\n *\n * @method fetchFooterData\n * @param {Number} courseid What course to fetch the data for\n * @param {Number} sectionid What section to fetch the data for\n * @return {object} jQuery promise\n */\nexport const fetchFooterData = (courseid, sectionid) => {\n const request = {\n methodname: 'core_course_get_activity_chooser_footer',\n args: {\n courseid: courseid,\n sectionid: sectionid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call","modName","modID","componentname","contentitemid","sectionid"],"mappings":";;;;;;;uPA+BgCA,iBACtBC,QAAU,CACZC,WAAY,uCACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU,6BAYD,CAACK,QAASC,eAC/BN,QAAU,CACZC,WAAY,kDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,+BAYC,CAACK,QAASC,eACjCN,QAAU,CACZC,WAAY,uDACZC,KAAM,CACFK,cAAeF,QACfG,cAAeF,eAGhBH,cAAKC,KAAK,CAACJ,UAAU,6BAWD,CAACD,SAAUU,mBAChCT,QAAU,CACZC,WAAY,0CACZC,KAAM,CACFH,SAAUA,SACVU,UAAWA,mBAGZN,cAAKC,KAAK,CAACJ,UAAU"} activitychooser/repository.min.js 0000604 00000002627 15062475555 0013343 0 ustar 00 define("core_course/local/activitychooser/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; /** * A javascript module to handle user AJAX actions. * * @module core_course/local/activitychooser/repository * @copyright 2019 Mathew May <mathew.solutions> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unfavouriteModule=_exports.fetchFooterData=_exports.favouriteModule=_exports.activityModules=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.activityModules=courseid=>{const request={methodname:"core_course_get_course_content_items",args:{courseid:courseid}};return _ajax.default.call([request])[0]};_exports.favouriteModule=(modName,modID)=>{const request={methodname:"core_course_add_content_item_to_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.unfavouriteModule=(modName,modID)=>{const request={methodname:"core_course_remove_content_item_from_user_favourites",args:{componentname:modName,contentitemid:modID}};return _ajax.default.call([request])[0]};_exports.fetchFooterData=(courseid,sectionid)=>{const request={methodname:"core_course_get_activity_chooser_footer",args:{courseid:courseid,sectionid:sectionid}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=repository.min.js.map activitychooser/dialogue.min.js 0000604 00000032460 15062475555 0012713 0 ustar 00 define("core_course/local/activitychooser/dialogue",["exports","jquery","core/modal_events","core_course/local/activitychooser/selectors","core/templates","core/key_codes","core/loadingicon","core_course/local/activitychooser/repository","core/notification","core/utils"],(function(_exports,_jquery,ModalEvents,_selectors,Templates,_key_codes,_loadingicon,Repository,_notification,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.displayChooser=void 0,_jquery=_interopRequireDefault(_jquery),ModalEvents=_interopRequireWildcard(ModalEvents),_selectors=_interopRequireDefault(_selectors),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),_notification=_interopRequireDefault(_notification);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const showModuleHelp=function(carousel,moduleData){let modal=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;null!==modal&&!0===moduleData.showFooter&&modal.setFooter(Templates.render("core_course/local/activitychooser/footer_partial",moduleData));const help=carousel.find(_selectors.default.regions.help)[0];help.innerHTML="",help.classList.add("m-auto");const spinnerPromise=(0,_loadingicon.addIconToContainer)(help);let transitionPromiseResolver=null;const transitionPromise=new Promise((resolve=>{transitionPromiseResolver=resolve})),contentPromise=Templates.renderForPromise("core_course/local/activitychooser/help",moduleData);Promise.all([contentPromise,spinnerPromise,transitionPromise]).then((_ref=>{let[{html:html,js:js}]=_ref;return Templates.replaceNodeContents(help,html,js)})).then((()=>(help.querySelector(_selectors.default.regions.chooserSummary.header).focus(),help))).catch(_notification.default.exception),carousel.one("slid.bs.carousel",(()=>{transitionPromiseResolver()})),carousel.carousel("next")},registerListenerEvents=(modal,mappedModules,partialFavourite,footerData)=>{const bodyClickListener=async e=>{if(e.target.closest(_selectors.default.actions.optionActions.showSummary)){const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel)),moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName);moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.target.closest(_selectors.default.actions.optionActions.manageFavourite)){const caller=e.target.closest(_selectors.default.actions.optionActions.manageFavourite);await(async(modalBody,caller,partialFavourite)=>{const isFavourite=caller.dataset.favourited,id=caller.dataset.id,name=caller.dataset.name,internal=caller.dataset.internal;"true"===isFavourite?(await Repository.unfavouriteModule(name,id),partialFavourite(internal,!1,modalBody)):(await Repository.favouriteModule(name,id),partialFavourite(internal,!0,modalBody))})(modal.getBody()[0],caller,partialFavourite);const activeSectionId=modal.getBody()[0].querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=modal.getBody()[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(modal.getBody()[0],mappedModules,sectionChooserOptions,modal)}if(e.target.matches(_selectors.default.actions.closeOption)){const carousel=(0,_jquery.default)(modal.getBody()[0].querySelector(_selectors.default.regions.carousel));carousel.carousel("prev"),carousel.on("slid.bs.carousel",(()=>{modal.getBody()[0].querySelector(_selectors.default.regions.modules).querySelector(_selectors.default.regions.getModuleSelector(e.target.dataset.modname)).focus()}))}if(e.target.closest(_selectors.default.actions.clearSearch)){const searchInput=modal.getBody()[0].querySelector(_selectors.default.actions.search);searchInput.value="",searchInput.focus(),toggleSearchResultsView(modal,mappedModules,searchInput.value)}},footerClickListener=async e=>{if(!0===footerData.footer){const footerjs=await(pluginName=footerData.customfooterjs,"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginName],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginName)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginName]));await footerjs.footerClickListener(e,footerData,modal)}var pluginName};modal.getBodyPromise().then((body=>body[0])).then((body=>((0,_jquery.default)(body.querySelector(_selectors.default.regions.carousel)).carousel({interval:!1,pause:!0,keyboard:!1}),body))).then((body=>(body.addEventListener("click",bodyClickListener),body))).then((body=>{const searchInput=body.querySelector(_selectors.default.actions.search);return searchInput.addEventListener("input",(0,_utils.debounce)((()=>{toggleSearchResultsView(modal,mappedModules,searchInput.value)}),300)),body})).then((body=>{const activeSectionId=body.querySelector(_selectors.default.elements.activetab).getAttribute("href"),sectionChooserOptions=body.querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=sectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container);return toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body,mappedModules,sectionChooserOptions,modal),body})).catch(),modal.getFooterPromise().then((footer=>footer[0])).then((footer=>(footer.addEventListener("click",footerClickListener),footer))).catch()},initChooserOptionsKeyboardNavigation=function(body,mappedModules,chooserOptionsContainer){let modal=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null;const chooserOptions=chooserOptionsContainer.querySelectorAll(_selectors.default.regions.chooserOption.container);Array.from(chooserOptions).forEach((element=>element.addEventListener("keydown",(e=>{if((e.keyCode===_key_codes.enter||e.keyCode===_key_codes.space)&&e.target.matches(_selectors.default.actions.optionActions.showSummary)){e.preventDefault();const moduleName=e.target.closest(_selectors.default.regions.chooserOption.container).dataset.modname,moduleData=mappedModules.get(moduleName),carousel=(0,_jquery.default)(body.querySelector(_selectors.default.regions.carousel));carousel.carousel({interval:!1,pause:!0,keyboard:!1}),moduleData.showFooter=modal.hasFooterContent(),showModuleHelp(carousel,moduleData,modal)}if(e.keyCode===_key_codes.arrowRight){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),nextOption=currentOption.nextElementSibling,firstOption=chooserOptionsContainer.firstElementChild,toFocusOption=clickErrorHandler(nextOption,firstOption);focusChooserOption(toFocusOption,currentOption)}if(e.keyCode===_key_codes.arrowLeft){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),previousOption=currentOption.previousElementSibling,lastOption=chooserOptionsContainer.lastElementChild,toFocusOption=clickErrorHandler(previousOption,lastOption);focusChooserOption(toFocusOption,currentOption)}if(e.keyCode===_key_codes.home){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),firstOption=chooserOptionsContainer.firstElementChild;focusChooserOption(firstOption,currentOption)}if(e.keyCode===_key_codes.end){e.preventDefault();const currentOption=e.target.closest(_selectors.default.regions.chooserOption.container),lastOption=chooserOptionsContainer.lastElementChild;focusChooserOption(lastOption,currentOption)}}))))},focusChooserOption=function(currentChooserOption){let previousChooserOption=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;null!==previousChooserOption&&toggleFocusableChooserOption(previousChooserOption,!1),toggleFocusableChooserOption(currentChooserOption,!0),currentChooserOption.focus()},toggleFocusableChooserOption=(chooserOption,isFocusable)=>{const chooserOptionLink=chooserOption.querySelector(_selectors.default.actions.addChooser),chooserOptionHelp=chooserOption.querySelector(_selectors.default.actions.optionActions.showSummary),chooserOptionFavourite=chooserOption.querySelector(_selectors.default.actions.optionActions.manageFavourite);isFocusable?(chooserOption.tabIndex=0,chooserOptionLink.tabIndex=0,chooserOptionHelp.tabIndex=0,chooserOptionFavourite.tabIndex=0):(chooserOption.tabIndex=-1,chooserOptionLink.tabIndex=-1,chooserOptionHelp.tabIndex=-1,chooserOptionFavourite.tabIndex=-1)},clickErrorHandler=(item,fallback)=>null!==item?item:fallback,toggleSearchResultsView=async(modal,mappedModules,searchQuery)=>{const modalBody=modal.getBody()[0],searchResultsContainer=modalBody.querySelector(_selectors.default.regions.searchResults),chooserContainer=modalBody.querySelector(_selectors.default.regions.chooser),clearSearchButton=modalBody.querySelector(_selectors.default.actions.clearSearch);if(searchQuery.length>0){const searchResultsData=searchModules(mappedModules,searchQuery);await(async(searchResultsContainer,searchResultsData)=>{const templateData={searchresultsnumber:searchResultsData.length,searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_course/local/activitychooser/search_results",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)})(searchResultsContainer,searchResultsData);const searchResultItemsContainer=searchResultsContainer.querySelector(_selectors.default.regions.searchResultItems),firstSearchResultItem=searchResultItemsContainer.querySelector(_selectors.default.regions.chooserOption.container);firstSearchResultItem&&(toggleFocusableChooserOption(firstSearchResultItem,!0),initChooserOptionsKeyboardNavigation(modalBody,mappedModules,searchResultItemsContainer,modal)),clearSearchButton.classList.remove("d-none"),chooserContainer.setAttribute("hidden","hidden"),searchResultsContainer.removeAttribute("hidden")}else clearSearchButton.classList.add("d-none"),searchResultsContainer.setAttribute("hidden","hidden"),chooserContainer.removeAttribute("hidden")},searchModules=(modules,searchTerm)=>{if(""===searchTerm)return modules;searchTerm=searchTerm.toLowerCase();const searchResults=[];return modules.forEach((activity=>{const activityName=activity.title.toLowerCase(),activityDesc=activity.help.toLowerCase();(activityName.includes(searchTerm)||activityDesc.includes(searchTerm))&&searchResults.push(activity)})),searchResults},disableFocusAllChooserOptions=sectionChooserOptions=>{sectionChooserOptions.querySelectorAll(_selectors.default.regions.chooserOption.container).forEach((chooserOption=>{toggleFocusableChooserOption(chooserOption,!1)}))};_exports.displayChooser=(modalPromise,sectionModules,partialFavourite,footerData)=>{const mappedModules=new Map;sectionModules.forEach((module=>{mappedModules.set(module.componentname+"_"+module.link,module)})),modalPromise.then((modal=>(registerListenerEvents(modal,mappedModules,partialFavourite,footerData),((modal,mappedModules)=>{modal.getModal()[0].tabIndex=-1,modal.getBodyPromise().then((body=>{(0,_jquery.default)(_selectors.default.elements.tab).on("shown.bs.tab",(e=>{const activeSectionId=e.target.getAttribute("href"),activeSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(activeSectionId)),firstChooserOption=activeSectionChooserOptions.querySelector(_selectors.default.regions.chooserOption.container),prevActiveSectionId=e.relatedTarget.getAttribute("href"),prevActiveSectionChooserOptions=body[0].querySelector(_selectors.default.regions.getSectionChooserOptions(prevActiveSectionId));disableFocusAllChooserOptions(prevActiveSectionChooserOptions),toggleFocusableChooserOption(firstChooserOption,!0),initChooserOptionsKeyboardNavigation(body[0],mappedModules,activeSectionChooserOptions,modal)}))})).catch(_notification.default.exception)})(modal,mappedModules),modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal))).catch()}})); //# sourceMappingURL=dialogue.min.js.map activitychooser/selectors.min.js.map 0000604 00000013443 15062475555 0013701 0 ustar 00 {"version":3,"file":"selectors.min.js","sources":["../../../src/local/activitychooser/selectors.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Define all of the selectors we will be using on the grading interface.\n *\n * @module core_course/local/activitychooser/selectors\n * @copyright 2019 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * A small helper function to build queryable data selectors.\n * @method getDataSelector\n * @param {String} name\n * @param {String} value\n * @return {string}\n */\nconst getDataSelector = (name, value) => {\n return `[data-${name}=\"${value}\"]`;\n};\n\nexport default {\n regions: {\n chooser: getDataSelector('region', 'chooser-container'),\n getSectionChooserOptions: containerid => `${containerid} ${getDataSelector('region', 'chooser-options-container')}`,\n chooserOption: {\n container: getDataSelector('region', 'chooser-option-container'),\n actions: getDataSelector('region', 'chooser-option-actions-container'),\n info: getDataSelector('region', 'chooser-option-info-container'),\n },\n chooserSummary: {\n container: getDataSelector('region', 'chooser-option-summary-container'),\n content: getDataSelector('region', 'chooser-option-summary-content-container'),\n header: getDataSelector('region', 'summary-header'),\n actions: getDataSelector('region', 'chooser-option-summary-actions-container'),\n },\n carousel: getDataSelector('region', 'carousel'),\n help: getDataSelector('region', 'help'),\n modules: getDataSelector('region', 'modules'),\n favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),\n defaultTabNav: getDataSelector('region', 'default-tab-nav'),\n activityTabNav: getDataSelector('region', 'activity-tab-nav'),\n favouriteTab: getDataSelector('region', 'favourites'),\n recommendedTab: getDataSelector('region', 'recommended'),\n defaultTab: getDataSelector('region', 'default'),\n activityTab: getDataSelector('region', 'activity'),\n resourceTab: getDataSelector('region', 'resources'),\n getModuleSelector: modname => `[role=\"menuitem\"][data-modname=\"${modname}\"]`,\n searchResults: getDataSelector('region', 'search-results-container'),\n searchResultItems: getDataSelector('region', 'search-result-items-container'),\n },\n actions: {\n optionActions: {\n showSummary: getDataSelector('action', 'show-option-summary'),\n manageFavourite: getDataSelector('action', 'manage-module-favourite'),\n },\n addChooser: getDataSelector('action', 'add-chooser-option'),\n closeOption: getDataSelector('action', 'close-chooser-option-summary'),\n hide: getDataSelector('action', 'hide'),\n search: getDataSelector('action', 'search'),\n clearSearch: getDataSelector('action', 'clearsearch'),\n },\n render: {\n favourites: getDataSelector('render', 'favourites-area'),\n },\n elements: {\n section: '.section',\n sectionmodchooser: 'button.section-modchooser-link',\n sitemenu: '.block_site_main_menu',\n sitetopic: 'div.sitetopic',\n tab: 'a[data-toggle=\"tab\"]',\n activetab: 'a[data-toggle=\"tab\"][aria-selected=\"true\"]',\n visibletabs: 'a[data-toggle=\"tab\"]:not(.d-none)'\n },\n};\n"],"names":["getDataSelector","name","value","regions","chooser","getSectionChooserOptions","containerid","chooserOption","container","actions","info","chooserSummary","content","header","carousel","help","modules","favouriteTabNav","defaultTabNav","activityTabNav","favouriteTab","recommendedTab","defaultTab","activityTab","resourceTab","getModuleSelector","modname","searchResults","searchResultItems","optionActions","showSummary","manageFavourite","addChooser","closeOption","hide","search","clearSearch","render","favourites","elements","section","sectionmodchooser","sitemenu","sitetopic","tab","activetab","visibletabs"],"mappings":";;;;;;;;MA8BMA,gBAAkB,CAACC,KAAMC,wBACXD,kBAASC,yBAGd,CACXC,QAAS,CACLC,QAASJ,gBAAgB,SAAU,qBACnCK,yBAA0BC,uBAAkBA,wBAAeN,gBAAgB,SAAU,8BACrFO,cAAe,CACXC,UAAWR,gBAAgB,SAAU,4BACrCS,QAAST,gBAAgB,SAAU,oCACnCU,KAAMV,gBAAgB,SAAU,kCAEpCW,eAAgB,CACZH,UAAWR,gBAAgB,SAAU,oCACrCY,QAASZ,gBAAgB,SAAU,4CACnCa,OAAQb,gBAAgB,SAAU,kBAClCS,QAAST,gBAAgB,SAAU,6CAEvCc,SAAUd,gBAAgB,SAAU,YACpCe,KAAMf,gBAAgB,SAAU,QAChCgB,QAAShB,gBAAgB,SAAU,WACnCiB,gBAAiBjB,gBAAgB,SAAU,qBAC3CkB,cAAelB,gBAAgB,SAAU,mBACzCmB,eAAgBnB,gBAAgB,SAAU,oBAC1CoB,aAAcpB,gBAAgB,SAAU,cACxCqB,eAAgBrB,gBAAgB,SAAU,eAC1CsB,WAAYtB,gBAAgB,SAAU,WACtCuB,YAAavB,gBAAgB,SAAU,YACvCwB,YAAaxB,gBAAgB,SAAU,aACvCyB,kBAAmBC,mDAA8CA,cACjEC,cAAe3B,gBAAgB,SAAU,4BACzC4B,kBAAmB5B,gBAAgB,SAAU,kCAEjDS,QAAS,CACLoB,cAAe,CACXC,YAAa9B,gBAAgB,SAAU,uBACvC+B,gBAAiB/B,gBAAgB,SAAU,4BAE/CgC,WAAYhC,gBAAgB,SAAU,sBACtCiC,YAAajC,gBAAgB,SAAU,gCACvCkC,KAAMlC,gBAAgB,SAAU,QAChCmC,OAAQnC,gBAAgB,SAAU,UAClCoC,YAAapC,gBAAgB,SAAU,gBAE3CqC,OAAQ,CACJC,WAAYtC,gBAAgB,SAAU,oBAE1CuC,SAAU,CACNC,QAAS,WACTC,kBAAmB,iCACnBC,SAAU,wBACVC,UAAW,gBACXC,IAAK,uBACLC,UAAW,6CACXC,YAAa"} attempt.php 0000604 00000037620 15062512377 0006747 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity attempt object * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local; use core_xapi\handler; use stdClass; use core_xapi\local\statement; /** * Class attempt for H5P activity * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class attempt { /** @var stdClass the h5pactivity_attempts record. */ private $record; /** @var boolean if the DB statement has been updated. */ private $scoreupdated = false; /** * Create a new attempt object. * * @param stdClass $record the h5pactivity_attempts record */ public function __construct(stdClass $record) { $this->record = $record; } /** * Create a new user attempt in a specific H5P activity. * * @param stdClass $user a user record * @param stdClass $cm a course_module record * @return attempt|null a new attempt object or null if fail */ public static function new_attempt(stdClass $user, stdClass $cm): ?attempt { global $DB; $record = new stdClass(); $record->h5pactivityid = $cm->instance; $record->userid = $user->id; $record->timecreated = time(); $record->timemodified = $record->timecreated; $record->rawscore = 0; $record->maxscore = 0; $record->duration = 0; $record->completion = null; $record->success = null; // Get last attempt number. $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id]; $countattempts = $DB->count_records('h5pactivity_attempts', $conditions); $record->attempt = $countattempts + 1; $record->id = $DB->insert_record('h5pactivity_attempts', $record); if (!$record->id) { return null; } // Remove any xAPI State associated to this attempt. $context = \context_module::instance($cm->id); $xapihandler = handler::create('mod_h5pactivity'); $xapihandler->wipe_states($context->id, $user->id); return new attempt($record); } /** * Get the last user attempt in a specific H5P activity. * * If no previous attempt exists, it generates a new one. * * @param stdClass $user a user record * @param stdClass $cm a course_module record * @return attempt|null a new attempt object or null if some problem accured */ public static function last_attempt(stdClass $user, stdClass $cm): ?attempt { global $DB; $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id]; $records = $DB->get_records('h5pactivity_attempts', $conditions, 'attempt DESC', '*', 0, 1); if (empty($records)) { return self::new_attempt($user, $cm); } return new attempt(array_shift($records)); } /** * Wipe all attempt data for specific course_module and an optional user. * * @param stdClass $cm a course_module record * @param stdClass $user a user record */ public static function delete_all_attempts(stdClass $cm, stdClass $user = null): void { global $DB; $where = 'a.h5pactivityid = :h5pactivityid'; $conditions = ['h5pactivityid' => $cm->instance]; if (!empty($user)) { $where .= ' AND a.userid = :userid'; $conditions['userid'] = $user->id; } $DB->delete_records_select('h5pactivity_attempts_results', "attemptid IN ( SELECT a.id FROM {h5pactivity_attempts} a WHERE $where)", $conditions); $DB->delete_records('h5pactivity_attempts', $conditions); } /** * Delete a specific attempt. * * @param attempt $attempt the attempt object to delete */ public static function delete_attempt(attempt $attempt): void { global $DB; $attempt->delete_results(); $DB->delete_records('h5pactivity_attempts', ['id' => $attempt->get_id()]); } /** * Save a new result statement into the attempt. * * It also updates the rawscore and maxscore if necessary. * * @param statement $statement the xAPI statement object * @param string $subcontent = '' optional subcontent identifier * @return bool if it can save the statement into db */ public function save_statement(statement $statement, string $subcontent = ''): bool { global $DB; // Check statement data. $xapiobject = $statement->get_object(); if (empty($xapiobject)) { return false; } $xapiresult = $statement->get_result(); $xapidefinition = $xapiobject->get_definition(); if (empty($xapidefinition) || empty($xapiresult)) { return false; } $xapicontext = $statement->get_context(); if ($xapicontext) { $context = $xapicontext->get_data(); } else { $context = new stdClass(); } $definition = $xapidefinition->get_data(); $result = $xapiresult->get_data(); $duration = $xapiresult->get_duration(); // Insert attempt_results record. $record = new stdClass(); $record->attemptid = $this->record->id; $record->subcontent = $subcontent; $record->timecreated = time(); $record->interactiontype = $definition->interactionType ?? 'other'; $record->description = $this->get_description_from_definition($definition); $record->correctpattern = $this->get_correctpattern_from_definition($definition); $record->response = $result->response ?? ''; $record->additionals = $this->get_additionals($definition, $context); $record->rawscore = 0; $record->maxscore = 0; if (isset($result->score)) { $record->rawscore = $result->score->raw ?? 0; $record->maxscore = $result->score->max ?? 0; } $record->duration = $duration; if (isset($result->completion)) { $record->completion = ($result->completion) ? 1 : 0; } if (isset($result->success)) { $record->success = ($result->success) ? 1 : 0; } if (!$DB->insert_record('h5pactivity_attempts_results', $record)) { return false; } // If no subcontent provided, results are propagated to the attempt itself. if (empty($subcontent)) { $this->set_duration($record->duration); $this->set_completion($record->completion ?? null); $this->set_success($record->success ?? null); // If Maxscore is not empty means that the rawscore is valid (even if it's 0) // and scaled score can be calculated. if ($record->maxscore) { $this->set_score($record->rawscore, $record->maxscore); } } // Refresh current attempt. return $this->save(); } /** * Update the current attempt record into DB. * * @return bool true if update is succesful */ public function save(): bool { global $DB; $this->record->timemodified = time(); // Calculate scaled score. if ($this->scoreupdated) { if (empty($this->record->maxscore)) { $this->record->scaled = 0; } else { $this->record->scaled = $this->record->rawscore / $this->record->maxscore; } } return $DB->update_record('h5pactivity_attempts', $this->record); } /** * Set the attempt score. * * @param int|null $rawscore the attempt rawscore * @param int|null $maxscore the attempt maxscore */ public function set_score(?int $rawscore, ?int $maxscore): void { $this->record->rawscore = $rawscore; $this->record->maxscore = $maxscore; $this->scoreupdated = true; } /** * Set the attempt duration. * * @param int|null $duration the attempt duration */ public function set_duration(?int $duration): void { $this->record->duration = $duration; } /** * Set the attempt completion. * * @param int|null $completion the attempt completion */ public function set_completion(?int $completion): void { $this->record->completion = $completion; } /** * Set the attempt success. * * @param int|null $success the attempt success */ public function set_success(?int $success): void { $this->record->success = $success; } /** * Delete the current attempt results from the DB. */ public function delete_results(): void { global $DB; $conditions = ['attemptid' => $this->record->id]; $DB->delete_records('h5pactivity_attempts_results', $conditions); } /** * Return de number of results stored in this attempt. * * @return int the number of results stored in this attempt. */ public function count_results(): int { global $DB; $conditions = ['attemptid' => $this->record->id]; return $DB->count_records('h5pactivity_attempts_results', $conditions); } /** * Return all results stored in this attempt. * * @return stdClass[] results records. */ public function get_results(): array { global $DB; $conditions = ['attemptid' => $this->record->id]; return $DB->get_records('h5pactivity_attempts_results', $conditions, 'id ASC'); } /** * Get additional data for some interaction types. * * @param stdClass $definition the statement object definition data * @param stdClass $context the statement optional context * @return string JSON encoded additional information */ private function get_additionals(stdClass $definition, stdClass $context): string { $additionals = []; $interactiontype = $definition->interactionType ?? 'other'; switch ($interactiontype) { case 'choice': case 'sequencing': $additionals['choices'] = $definition->choices ?? []; break; case 'matching': $additionals['source'] = $definition->source ?? []; $additionals['target'] = $definition->target ?? []; break; case 'likert': $additionals['scale'] = $definition->scale ?? []; break; case 'performance': $additionals['steps'] = $definition->steps ?? []; break; } $additionals['extensions'] = $definition->extensions ?? new stdClass(); // Add context extensions. $additionals['contextExtensions'] = $context->extensions ?? new stdClass(); if (empty($additionals)) { return ''; } return json_encode($additionals); } /** * Extract the result description from statement object definition. * * In principle, H5P package can send a multilang description but the reality * is that most activities only send the "en_US" description if any and the * activity does not have any control over it. * * @param stdClass $definition the statement object definition * @return string The available description if any */ private function get_description_from_definition(stdClass $definition): string { if (!isset($definition->description)) { return ''; } $translations = (array) $definition->description; if (empty($translations)) { return ''; } // By default, H5P packages only send "en-US" descriptions. return $translations['en-US'] ?? array_shift($translations); } /** * Extract the correct pattern from statement object definition. * * The correct pattern depends on the type of content and the plugin * has no control over it so we just store it in case that the statement * data have it. * * @param stdClass $definition the statement object definition * @return string The correct pattern if any */ private function get_correctpattern_from_definition(stdClass $definition): string { if (!isset($definition->correctResponsesPattern)) { return ''; } // Only arrays are allowed. if (is_array($definition->correctResponsesPattern)) { return json_encode($definition->correctResponsesPattern); } return ''; } /** * Return the attempt number. * * @return int the attempt number */ public function get_attempt(): int { return $this->record->attempt; } /** * Return the attempt ID. * * @return int the attempt id */ public function get_id(): int { return $this->record->id; } /** * Return the attempt user ID. * * @return int the attempt userid */ public function get_userid(): int { return $this->record->userid; } /** * Return the attempt H5P timecreated. * * @return int the attempt timecreated */ public function get_timecreated(): int { return $this->record->timecreated; } /** * Return the attempt H5P timemodified. * * @return int the attempt timemodified */ public function get_timemodified(): int { return $this->record->timemodified; } /** * Return the attempt H5P activity ID. * * @return int the attempt userid */ public function get_h5pactivityid(): int { return $this->record->h5pactivityid; } /** * Return the attempt maxscore. * * @return int the maxscore value */ public function get_maxscore(): int { return $this->record->maxscore; } /** * Return the attempt rawscore. * * @return int the rawscore value */ public function get_rawscore(): int { return $this->record->rawscore; } /** * Return the attempt duration. * * @return int|null the duration value */ public function get_duration(): ?int { return $this->record->duration; } /** * Return the attempt completion. * * @return int|null the completion value */ public function get_completion(): ?int { return $this->record->completion; } /** * Return the attempt success. * * @return int|null the success value */ public function get_success(): ?int { return $this->record->success; } /** * Return the attempt scaled. * * @return int|null the scaled value */ public function get_scaled(): ?int { return is_null($this->record->scaled) ? $this->record->scaled : (int)$this->record->scaled; } /** * Return if the attempt has been modified. * * Note: adding a result only add track information unless the statement does * not specify subcontent. In this case this will update also the statement. * * @return bool if the attempt score have been modified */ public function get_scoreupdated(): bool { return $this->scoreupdated; } } grader.php 0000604 00000016625 15062512377 0006537 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity grader class. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local; use context_module; use cm_info; use moodle_recordset; use stdClass; /** * Class for handling H5P activity grading. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grader { /** @var stdClass course_module record. */ private $instance; /** @var string idnumber course_modules idnumber. */ private $idnumber; /** * Class contructor. * * @param stdClass $instance H5Pactivity instance object * @param string $idnumber course_modules idnumber */ public function __construct(stdClass $instance, string $idnumber = '') { $this->instance = $instance; $this->idnumber = $idnumber; } /** * Delete grade item for given mod_h5pactivity instance. * * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED */ public function grade_item_delete(): ?int { global $CFG; require_once($CFG->libdir.'/gradelib.php'); return grade_update('mod/h5pactivity', $this->instance->course, 'mod', 'h5pactivity', $this->instance->id, 0, null, ['deleted' => 1]); } /** * Creates or updates grade item for the given mod_h5pactivity instance. * * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int 0 if ok, error code otherwise */ public function grade_item_update($grades = null): int { global $CFG; require_once($CFG->libdir.'/gradelib.php'); $item = []; $item['itemname'] = clean_param($this->instance->name, PARAM_NOTAGS); $item['gradetype'] = GRADE_TYPE_VALUE; if (!empty($this->idnumber)) { $item['idnumber'] = $this->idnumber; } if ($this->instance->grade > 0) { $item['gradetype'] = GRADE_TYPE_VALUE; $item['grademax'] = $this->instance->grade; $item['grademin'] = 0; } else if ($this->instance->grade < 0) { $item['gradetype'] = GRADE_TYPE_SCALE; $item['scaleid'] = -$this->instance->grade; } else { $item['gradetype'] = GRADE_TYPE_NONE; } if ($grades === 'reset') { $item['reset'] = true; $grades = null; } return grade_update('mod/h5pactivity', $this->instance->course, 'mod', 'h5pactivity', $this->instance->id, 0, $grades, $item); } /** * Update grades in the gradebook. * * @param int $userid Update grade of specific user only, 0 means all participants. */ public function update_grades(int $userid = 0): void { // Scaled and none grading doesn't have grade calculation. if ($this->instance->grade <= 0) { $this->grade_item_update(); return; } // Populate array of grade objects indexed by userid. $grades = $this->get_user_grades_for_gradebook($userid); if (!empty($grades)) { $this->grade_item_update($grades); } else { $this->grade_item_update(); } } /** * Get an updated list of user grades and feedback for the gradebook. * * @param int $userid int or 0 for all users * @return array of grade data formated for the gradebook api * The data required by the gradebook api is userid, * rawgrade, * feedback, * feedbackformat, * usermodified, * dategraded, * datesubmitted */ private function get_user_grades_for_gradebook(int $userid = 0): array { $grades = []; // In case of using manual grading this update must delete previous automatic gradings. if ($this->instance->grademethod == manager::GRADEMANUAL || !$this->instance->enabletracking) { return $this->get_user_grades_for_deletion($userid); } $manager = manager::create_from_instance($this->instance); $scores = $manager->get_users_scaled_score($userid); if (!$scores) { return $grades; } // Maxgrade depends on the type of grade used: // - grade > 0: regular quantitative grading. // - grade = 0: no grading. // - grade < 0: scale used. $maxgrade = floatval($this->instance->grade); // Convert scaled scores into gradebok compatible objects. foreach ($scores as $userid => $score) { $grades[$userid] = [ 'userid' => $userid, 'rawgrade' => $maxgrade * $score->scaled, 'dategraded' => $score->timemodified, 'datesubmitted' => $score->timemodified, ]; } return $grades; } /** * Get an deletion list of user grades and feedback for the gradebook. * * This method is used to delete all autmatic gradings when grading method is set to manual. * * @param int $userid int or 0 for all users * @return array of grade data formated for the gradebook api * The data required by the gradebook api is userid, * rawgrade (null to delete), * dategraded, * datesubmitted */ private function get_user_grades_for_deletion (int $userid = 0): array { $grades = []; if ($userid) { $grades[$userid] = [ 'userid' => $userid, 'rawgrade' => null, 'dategraded' => time(), 'datesubmitted' => time(), ]; } else { $manager = manager::create_from_instance($this->instance); $users = get_enrolled_users($manager->get_context(), 'mod/h5pactivity:submit'); foreach ($users as $user) { $grades[$user->id] = [ 'userid' => $user->id, 'rawgrade' => null, 'dategraded' => time(), 'datesubmitted' => time(), ]; } } return $grades; } } report/results.php 0000604 00000006173 15062512377 0010304 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity results report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local\report; use mod_h5pactivity\local\report; use mod_h5pactivity\local\manager; use mod_h5pactivity\local\attempt; use mod_h5pactivity\output\reportresults; use stdClass; /** * Class H5P activity results report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> */ class results implements report { /** @var manager the H5P activity manager instance. */ private $manager; /** @var stdClass the user record. */ private $user; /** @var attempt the h5pactivity attempt to show. */ private $attempt; /** * Create a new participants report. * * @param manager $manager h5pactivity manager object * @param stdClass $user user record * @param attempt $attempt attempt object */ public function __construct(manager $manager, stdClass $user, attempt $attempt) { $this->manager = $manager; $this->user = $user; $this->attempt = $attempt; } /** * Return the report user record. * * @return stdClass|null a user or null */ public function get_user(): ?stdClass { return $this->user; } /** * Return the report attempt object. * * Attempts report has no specific attempt. * * @return attempt|null the attempt object or null */ public function get_attempt(): ?attempt { return $this->attempt; } /** * Print the report. */ public function print(): void { global $OUTPUT; $manager = $this->manager; $attempt = $this->attempt; $cm = $manager->get_coursemodule(); $widget = new reportresults($attempt, $this->user, $cm->course); echo $OUTPUT->render($widget); } /** * Get the export data form this report. * * This method is used to render the report in mobile. */ public function export_data_for_external(): stdClass { global $PAGE; $manager = $this->manager; $attempt = $this->attempt; $cm = $manager->get_coursemodule(); $widget = new reportresults($attempt, $this->user, $cm->course); return $widget->export_for_template($PAGE->get_renderer('core')); } } report/attempts.php 0000604 00000007616 15062512377 0010447 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity attempts report * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local\report; use mod_h5pactivity\local\report; use mod_h5pactivity\local\manager; use mod_h5pactivity\local\attempt; use mod_h5pactivity\output\reportattempts; use stdClass; /** * Class H5P activity attempts report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> */ class attempts implements report { /** @var manager the H5P activity manager instance. */ private $manager; /** @var stdClass the user record. */ private $user; /** * Create a new participants report. * * @param manager $manager h5pactivity manager object * @param stdClass $user user record */ public function __construct(manager $manager, stdClass $user) { $this->manager = $manager; $this->user = $user; } /** * Return the report user record. * * @return stdClass|null a user or null */ public function get_user(): ?stdClass { return $this->user; } /** * Return the report attempt object. * * Attempts report has no specific attempt. * * @return attempt|null the attempt object or null */ public function get_attempt(): ?attempt { return null; } /** * Print the report. */ public function print(): void { global $OUTPUT; $manager = $this->manager; $cm = $manager->get_coursemodule(); $scored = $this->get_scored(); $title = $scored->title ?? null; $scoredattempt = $scored->attempt ?? null; $attempts = $this->get_attempts(); $widget = new reportattempts($attempts, $this->user, $cm->course, $title, $scoredattempt); echo $OUTPUT->render($widget); } /** * Return the current report attempts. * * This method is used to render the report in both browser and mobile. * * @return attempts[] */ public function get_attempts(): array { return $this->manager->get_user_attempts($this->user->id); } /** * Return the current report attempts. * * This method is used to render the report in both browser and mobile. * * @return stdClass|null a structure with * - title => name of the selected attempt (or null) * - attempt => the selected attempt object (or null) * - gradingmethos => the activity grading method (or null) */ public function get_scored(): ?stdClass { $manager = $this->manager; $scores = $manager->get_users_scaled_score($this->user->id); $score = $scores[$this->user->id] ?? null; if (empty($score->attemptid)) { return null; } list($grademethod, $title) = $manager->get_selected_attempt(); $scoredattempt = $manager->get_attempt($score->attemptid); $result = (object)[ 'title' => $title, 'attempt' => $scoredattempt, 'grademethod' => $grademethod, ]; return $result; } } report/participants.php 0000604 00000014373 15062512377 0011305 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity participants report * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local\report; use mod_h5pactivity\local\report; use mod_h5pactivity\local\manager; use mod_h5pactivity\local\attempt; use core\dml\sql_join; use table_sql; use moodle_url; use html_writer; use stdClass; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir.'/tablelib.php'); /** * Class H5P activity participants report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> */ class participants extends table_sql implements report { /** @var manager the H5P activity manager instance. */ private $manager; /** @var array the users scored attempts. */ private $scores; /** @var array the user attempts count. */ private $count; /** * Create a new participants report. * * @param manager $manager h5pactivitymanager object * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group */ public function __construct(manager $manager, $currentgroup = false) { parent::__construct('mod_h5pactivity-participants'); $this->manager = $manager; $this->scores = $manager->get_users_scaled_score(); $this->count = $manager->count_users_attempts(); // Setup table_sql. $columns = ['fullname', 'timemodified', 'score', 'attempts']; $headers = [ get_string('fullname'), get_string('date'), get_string('score', 'mod_h5pactivity'), get_string('attempts', 'mod_h5pactivity'), ]; $this->define_columns($columns); $this->define_headers($headers); $this->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide'); $this->sortable(true); $this->no_sorting('score'); $this->no_sorting('timemodified'); $this->no_sorting('attempts'); $this->pageable(true); $capjoin = $this->manager->get_active_users_join(true, $currentgroup); // Final SQL. $this->set_sql( 'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic, u.middlename, u.alternatename, u.imagealt, u.email', "{user} u $capjoin->joins", $capjoin->wheres, $capjoin->params); } /** * Return the report user record. * * Participants report has no specific user. * * @return stdClass|null a user or null */ public function get_user(): ?stdClass { return null; } /** * Return the report attempt object. * * Participants report has no specific attempt. * * @return attempt|null the attempt object or null */ public function get_attempt(): ?attempt { return null; } /** * Print the report. */ public function print(): void { global $PAGE, $OUTPUT; $this->define_baseurl($PAGE->url); $this->out($this->get_page_size(), true); } /** * Warning in case no user has the selected initials letters. * */ public function print_nothing_to_display() { global $OUTPUT; echo $this->render_reset_button(); $this->print_initials_bar(); echo $OUTPUT->notification(get_string('noparticipants', 'mod_h5pactivity'), 'warning'); } /** * Generate the fullname column. * * @param stdClass $user * @return string */ public function col_fullname($user): string { global $OUTPUT; $cm = $this->manager->get_coursemodule(); return $OUTPUT->user_picture($user, ['size' => 35, 'courseid' => $cm->course, 'includefullname' => true]); } /** * Generate score column. * * @param stdClass $user the user record * @return string */ public function col_score(stdClass $user): string { $cm = $this->manager->get_coursemodule(); if (isset($this->scores[$user->id])) { $score = $this->scores[$user->id]; $maxgrade = floatval(100); $scaled = round($maxgrade * $score->scaled).'%'; if (empty($score->attemptid)) { return $scaled; } else { $url = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'attemptid' => $score->attemptid]); return html_writer::link($url, $scaled); } } return ''; } /** * Generate attempts count column, if any. * * @param stdClass $user the user record * @return string */ public function col_attempts(stdClass $user): string { $cm = $this->manager->get_coursemodule(); if (isset($this->count[$user->id])) { $msg = get_string('review_user_attempts', 'mod_h5pactivity', $this->count[$user->id]); $url = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'userid' => $user->id]); return html_writer::link($url, $msg); } return ''; } /** * Generate attempt timemodified column, if any. * * @param stdClass $user the user record * @return string */ public function col_timemodified(stdClass $user): string { if (isset($this->scores[$user->id])) { $score = $this->scores[$user->id]; return userdate($score->timemodified); } return ''; } } report.php 0000604 00000003046 15062512377 0006577 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity report interface * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local; use templatable; use stdClass; /** * Interface for any mod_h5pactivity report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> */ interface report { /** * Return the report user record. * * @return stdClass|null a user or null */ public function get_user(): ?stdClass; /** * Return the report attempt object. * * @return attempt|null the attempt object or null */ public function get_attempt(): ?attempt; /** * Print the report visualization. */ public function print(): void; } manager.php 0000604 00000046660 15062512377 0006707 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * H5P activity manager class * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local; use mod_h5pactivity\local\report\participants; use mod_h5pactivity\local\report\attempts; use mod_h5pactivity\local\report\results; use context_module; use cm_info; use moodle_recordset; use core_user; use stdClass; use core\dml\sql_join; use mod_h5pactivity\event\course_module_viewed; /** * Class manager for H5P activity * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** No automathic grading using attempt results. */ const GRADEMANUAL = 0; /** Use highest attempt results for grading. */ const GRADEHIGHESTATTEMPT = 1; /** Use average attempt results for grading. */ const GRADEAVERAGEATTEMPT = 2; /** Use last attempt results for grading. */ const GRADELASTATTEMPT = 3; /** Use first attempt results for grading. */ const GRADEFIRSTATTEMPT = 4; /** Participants cannot review their own attempts. */ const REVIEWNONE = 0; /** Participants can review their own attempts when have one attempt completed. */ const REVIEWCOMPLETION = 1; /** @var stdClass course_module record. */ private $instance; /** @var context_module the current context. */ private $context; /** @var cm_info course_modules record. */ private $coursemodule; /** * Class contructor. * * @param cm_info $coursemodule course module info object * @param stdClass $instance H5Pactivity instance object. */ public function __construct(cm_info $coursemodule, stdClass $instance) { $this->coursemodule = $coursemodule; $this->instance = $instance; $this->context = context_module::instance($coursemodule->id); $this->instance->cmidnumber = $coursemodule->idnumber; } /** * Create a manager instance from an instance record. * * @param stdClass $instance a h5pactivity record * @return manager */ public static function create_from_instance(stdClass $instance): self { $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id); // Ensure that $this->coursemodule is a cm_info object. $coursemodule = cm_info::create($coursemodule); return new self($coursemodule, $instance); } /** * Create a manager instance from an course_modules record. * * @param stdClass|cm_info $coursemodule a h5pactivity record * @return manager */ public static function create_from_coursemodule($coursemodule): self { global $DB; // Ensure that $this->coursemodule is a cm_info object. $coursemodule = cm_info::create($coursemodule); $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST); return new self($coursemodule, $instance); } /** * Return the available grading methods. * @return string[] an array "option value" => "option description" */ public static function get_grading_methods(): array { return [ self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'), self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'), self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'), self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'), self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'), ]; } /** * Return the selected attempt criteria. * @return string[] an array "grademethod value", "attempt description" */ public function get_selected_attempt(): array { $types = [ self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'), self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'), self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'), self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'), self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'), ]; if ($this->instance->enabletracking) { $key = $this->instance->grademethod; } else { $key = self::GRADEMANUAL; } return [$key, $types[$key]]; } /** * Return the available review modes. * * @return string[] an array "option value" => "option description" */ public static function get_review_modes(): array { return [ self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'), self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'), ]; } /** * Check if tracking is enabled in a particular h5pactivity for a specific user. * * @param stdClass|null $user user record (default $USER) * @return bool if tracking is enabled in this activity */ public function is_tracking_enabled(stdClass $user = null): bool { global $USER; if (!$this->instance->enabletracking) { return false; } if (empty($user)) { $user = $USER; } return has_capability('mod/h5pactivity:submit', $this->context, $user, false); } /** * Check if a user can see the activity attempts list. * * @param stdClass|null $user user record (default $USER) * @return bool if the user can see the attempts link */ public function can_view_all_attempts(stdClass $user = null): bool { global $USER; if (!$this->instance->enabletracking) { return false; } if (empty($user)) { $user = $USER; } return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user); } /** * Check if a user can see own attempts. * * @param stdClass|null $user user record (default $USER) * @return bool if the user can see the own attempts link */ public function can_view_own_attempts(stdClass $user = null): bool { global $USER; if (!$this->instance->enabletracking) { return false; } if (empty($user)) { $user = $USER; } if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) { return true; } if ($this->instance->reviewmode == self::REVIEWNONE) { return false; } if ($this->instance->reviewmode == self::REVIEWCOMPLETION) { return true; } return false; } /** * Return a relation of userid and the valid attempt's scaled score. * * The returned elements contain a record * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT" * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL" * the method will return null. * * @param int $userid a specific userid or 0 for all user attempts. * @return array|null of userid, scaled value and, if exists, the attempt id */ public function get_users_scaled_score(int $userid = 0): ?array { global $DB; $scaled = []; if (!$this->instance->enabletracking) { return null; } if ($this->instance->grademethod == self::GRADEMANUAL) { return null; } $sql = ''; // General filter. $where = 'a.h5pactivityid = :h5pactivityid'; $params['h5pactivityid'] = $this->instance->id; if ($userid) { $where .= ' AND a.userid = :userid'; $params['userid'] = $userid; } // Average grading needs aggregation query. if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) { $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified FROM {h5pactivity_attempts} a WHERE $where AND a.completion = 1 GROUP BY a.userid"; } if (empty($sql)) { // Decide which attempt is used for the calculation. $condition = [ self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled", self::GRADELASTATTEMPT => "a.attempt < b.attempt", self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt", ]; $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT]; $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified FROM {h5pactivity_attempts} a LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid AND a.userid = b.userid AND b.completion = 1 AND $join WHERE $where AND b.id IS NULL AND a.completion = 1 GROUP BY a.userid, a.scaled"; } return $DB->get_records_sql($sql, $params); } /** * Count the activity completed attempts. * * If no user is provided the method will count all active users attempts. * Check get_active_users_join PHPdoc to a more detailed description of "active users". * * @param int|null $userid optional user id (default null) * @return int the total amount of attempts */ public function count_attempts(int $userid = null): int { global $DB; // Counting records is enough for one user. if ($userid) { $params['userid'] = $userid; $params = [ 'h5pactivityid' => $this->instance->id, 'userid' => $userid, 'completion' => 1, ]; return $DB->count_records('h5pactivity_attempts', $params); } $usersjoin = $this->get_active_users_join(); // Final SQL. return $DB->count_records_sql( "SELECT COUNT(*) FROM {user} u $usersjoin->joins WHERE $usersjoin->wheres", array_merge($usersjoin->params) ); } /** * Return the join to collect all activity active users. * * The concept of active user is relative to the activity permissions. All users with * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts" * are evaluators and they don't count as valid submitters. * * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit" * but submit capability cannot be used because is a write capability and does not apply to frozen contexts. * * @since Moodle 3.11 * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts. * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group * @return sql_join the active users attempts join */ public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join { // Only valid users counts. By default, all users with submit capability are considered potential ones. $context = $this->get_context(); $coursemodule = $this->get_coursemodule(); // Ensure user can view users from all groups. if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { return new sql_join('', '1=2', [], true); } // We want to present all potential users. $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup); if ($capjoin->cannotmatchanyrows) { return $capjoin; } // But excluding all reviewattempts users converting a capabilities join into left join. $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id'); if ($reviewersjoin->cannotmatchanyrows) { return $capjoin; } $capjoin = new sql_join( $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins), $capjoin->wheres . " AND reviewer.userid IS NULL", $capjoin->params ); if ($allpotentialusers) { return $capjoin; } // Add attempts join. $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion"; $params = [ 'h5pactivityid' => $this->instance->id, 'completion' => 1, ]; return new sql_join( $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id", $capjoin->wheres . " AND $where", array_merge($capjoin->params, $params) ); } /** * Return an array of all users and it's total attempts. * * Note: this funciton only returns the list of users with attempts, * it does not check all participants. * * @return array indexed count userid => total number of attempts */ public function count_users_attempts(): array { global $DB; $params = [ 'h5pactivityid' => $this->instance->id, ]; $sql = "SELECT userid, count(*) FROM {h5pactivity_attempts} WHERE h5pactivityid = :h5pactivityid GROUP BY userid"; return $DB->get_records_sql_menu($sql, $params); } /** * Return the current context. * * @return context_module */ public function get_context(): context_module { return $this->context; } /** * Return the current instance. * * @return stdClass the instance record */ public function get_instance(): stdClass { return $this->instance; } /** * Return the current cm_info. * * @return cm_info the course module */ public function get_coursemodule(): cm_info { return $this->coursemodule; } /** * Return the specific grader object for this activity. * * @return grader */ public function get_grader(): grader { $idnumber = $this->coursemodule->idnumber ?? ''; return new grader($this->instance, $idnumber); } /** * Return the suitable report to show the attempts. * * This method controls the access to the different reports * the activity have. * * @param int $userid an opional userid to show * @param int $attemptid an optional $attemptid to show * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group * @return report|null available report (or null if no report available) */ public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report { global $USER, $CFG; require_once("{$CFG->dirroot}/user/lib.php"); // If tracking is disabled, no reports are available. if (!$this->instance->enabletracking) { return null; } $attempt = null; if ($attemptid) { $attempt = $this->get_attempt($attemptid); if (!$attempt) { return null; } // If we have and attempt we can ignore the provided $userid. $userid = $attempt->get_userid(); } if ($this->can_view_all_attempts()) { $user = core_user::get_user($userid); // Ensure user can view the attempt of specific userid, respecting access checks. if ($user && $user->id != $USER->id) { $course = get_course($this->coursemodule->course); if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) { return null; } } } else if ($this->can_view_own_attempts()) { $user = core_user::get_user($USER->id); if ($userid && $user->id != $userid) { return null; } } else { return null; } // Only enrolled users has reports. if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) { return null; } // Create the proper report. if ($user && $attempt) { return new results($this, $user, $attempt); } else if ($user) { return new attempts($this, $user); } return new participants($this, $currentgroup); } /** * Return a single attempt. * * @param int $attemptid the attempt id * @return attempt */ public function get_attempt(int $attemptid): ?attempt { global $DB; $record = $DB->get_record('h5pactivity_attempts', [ 'id' => $attemptid, 'h5pactivityid' => $this->instance->id, ]); if (!$record) { return null; } return new attempt($record); } /** * Return an array of all user attempts (including incompleted) * * @param int $userid the user id * @return attempt[] */ public function get_user_attempts(int $userid): array { global $DB; $records = $DB->get_records( 'h5pactivity_attempts', ['userid' => $userid, 'h5pactivityid' => $this->instance->id], 'id ASC' ); if (!$records) { return []; } $result = []; foreach ($records as $record) { $result[] = new attempt($record); } return $result; } /** * Trigger module viewed event and set the module viewed for completion. * * @param stdClass $course course object * @return void */ public function set_module_viewed(stdClass $course): void { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Trigger module viewed event. $event = course_module_viewed::create([ 'objectid' => $this->instance->id, 'context' => $this->context ]); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('course_modules', $this->coursemodule); $event->add_record_snapshot('h5pactivity', $this->instance); $event->trigger(); // Completion. $completion = new \completion_info($course); $completion->set_module_viewed($this->coursemodule); } } entities/badge_issued.php 0000604 00000011347 15062573052 0011526 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_badges\reportbuilder\local\entities; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{boolean_select, date}; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\{column, filter}; /** * Badge issued entity * * @package core_badges * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badge_issued extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return ['badge_issued' => 'bi']; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('badgeissued', 'core_badges'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $badgeissuedalias = $this->get_table_alias('badge_issued'); // Date issued. $columns[] = (new column( 'issued', new lang_string('dateawarded', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$badgeissuedalias}.dateissued") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Date expires. $columns[] = (new column( 'expire', new lang_string('expirydate', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_field("{$badgeissuedalias}.dateexpire") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Visible. $columns[] = (new column( 'visible', new lang_string('visible', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_BOOLEAN) ->add_fields("{$badgeissuedalias}.visible") ->add_callback([format::class, 'boolean_as_text']); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $badgealias = $this->get_table_alias('badge_issued'); // Date issued. $filters[] = (new filter( date::class, 'issued', new lang_string('dateawarded', 'core_badges'), $this->get_entity_name(), "{$badgealias}.dateissued" )) ->add_joins($this->get_joins()); // Date expires. $filters[] = (new filter( date::class, 'expires', new lang_string('expirydate', 'core_badges'), $this->get_entity_name(), "{$badgealias}.dateexpire" )) ->add_joins($this->get_joins()); // Visible. $filters[] = (new filter( boolean_select::class, 'visible', new lang_string('visible', 'core_badges'), $this->get_entity_name(), "{$badgealias}.visible" )) ->add_joins($this->get_joins()); return $filters; } } entities/badge.php 0000604 00000026655 15062573052 0010162 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_badges\reportbuilder\local\entities; use context_course; use context_helper; use context_system; use html_writer; use lang_string; use moodle_url; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\{select, text}; use core_reportbuilder\local\report\{column, filter}; defined('MOODLE_INTERNAL') or die; global $CFG; require_once("{$CFG->libdir}/badgeslib.php"); /** * Badge entity * * @package core_badges * @copyright 2022 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badge extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'badge' => 'b', 'context' => 'bctx', 'tag_instance' => 'bti', 'tag' => 'bt', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('badgedetails', 'core_badges'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $badgealias = $this->get_table_alias('badge'); $contextalias = $this->get_table_alias('context'); // Name. $columns[] = (new column( 'name', new lang_string('name'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.name") ->set_is_sortable(true); // Name with link. $columns[] = (new column( 'namewithlink', new lang_string('namewithlink', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$badgealias}.name, {$badgealias}.id") ->set_is_sortable(true) ->add_callback(static function(?string $value, stdClass $row): string { if (!$row->id) { return ''; } $url = new moodle_url('/badges/overview.php', ['id' => $row->id]); return html_writer::link($url, $row->name); }); // Description (note, this column contains plaintext so requires no post-processing). $descriptionfieldsql = "{$badgealias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description'); // Criteria. $columns[] = (new column( 'criteria', new lang_string('bcriteria', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.id") ->set_disabled_aggregation_all() ->add_callback(static function($badgeid): string { global $PAGE; if (!$badgeid) { return ''; } $badge = new \core_badges\badge($badgeid); $renderer = $PAGE->get_renderer('core_badges'); return $renderer->print_badge_criteria($badge, 'short'); }); // Image. $columns[] = (new column( 'image', new lang_string('badgeimage', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("LEFT JOIN {context} {$contextalias} ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . " AND {$contextalias}.instanceid = {$badgealias}.courseid") ->set_type(column::TYPE_INTEGER) ->add_fields("{$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid") ->add_field($DB->sql_cast_to_char("{$badgealias}.imagecaption"), 'imagecaption') ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_disabled_aggregation_all() ->add_callback(static function(?int $badgeid, stdClass $badge): string { if (!$badgeid) { return ''; } if ($badge->type == BADGE_TYPE_SITE) { $context = context_system::instance(); } else { context_helper::preload_from_record($badge); $context = context_course::instance($badge->courseid); } $badgeimage = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badgeid, '/', 'f2'); return html_writer::img($badgeimage, $badge->imagecaption); }); // Language. $columns[] = (new column( 'language', new lang_string('language'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.language") ->set_is_sortable(true) ->add_callback(static function($language): string { $languages = get_string_manager()->get_list_of_languages(); return $languages[$language] ?? $language ?? ''; }); // Version. $columns[] = (new column( 'version', new lang_string('version', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.version") ->set_is_sortable(true); // Status. $columns[] = (new column( 'status', new lang_string('status', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.status") ->set_is_sortable(true) ->add_callback(static function($status): string { if ($status === null) { return ''; } return get_string("badgestatus_{$status}", 'core_badges'); }); // Expiry date/period. $columns[] = (new column( 'expiry', new lang_string('expirydate', 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$badgealias}.expiredate, {$badgealias}.expireperiod, {$badgealias}.id") ->set_is_sortable(true, ["{$badgealias}.expiredate", "{$badgealias}.expireperiod"]) ->set_disabled_aggregation_all() ->add_callback(static function(?int $expiredate, stdClass $badge): string { if (!$badge->id) { return ''; } else if ($expiredate) { return userdate($expiredate); } else if ($badge->expireperiod) { return format_time($badge->expireperiod); } else { return get_string('never', 'core_badges'); } }); // Image author details. foreach (['imageauthorname', 'imageauthoremail', 'imageauthorurl'] as $imageauthorfield) { $columns[] = (new column( $imageauthorfield, new lang_string($imageauthorfield, 'core_badges'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$badgealias}.{$imageauthorfield}") ->set_is_sortable(true); } return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $badgealias = $this->get_table_alias('badge'); // Name. $filters[] = (new filter( text::class, 'name', new lang_string('name'), $this->get_entity_name(), "{$badgealias}.name" )) ->add_joins($this->get_joins()); // Status. $filters[] = (new filter( select::class, 'status', new lang_string('status', 'core_badges'), $this->get_entity_name(), "{$badgealias}.status" )) ->add_joins($this->get_joins()) ->set_options([ BADGE_STATUS_INACTIVE => new lang_string('badgestatus_0', 'core_badges'), BADGE_STATUS_ACTIVE => new lang_string('badgestatus_1', 'core_badges'), BADGE_STATUS_INACTIVE_LOCKED => new lang_string('badgestatus_2', 'core_badges'), BADGE_STATUS_ACTIVE_LOCKED => new lang_string('badgestatus_3', 'core_badges'), BADGE_STATUS_ARCHIVED => new lang_string('badgestatus_4', 'core_badges'), ]); // Type. $filters[] = (new filter( select::class, 'type', new lang_string('type', 'core_badges'), $this->get_entity_name(), "{$badgealias}.type" )) ->add_joins($this->get_joins()) ->set_options([ BADGE_TYPE_SITE => new lang_string('site'), BADGE_TYPE_COURSE => new lang_string('course'), ]); return $filters; } /** * Return joins necessary for retrieving tags * * @return string[] */ public function get_tag_joins(): array { return $this->get_tag_joins_for_entity('core_badges', 'badge', $this->get_table_alias('badge') . '.id'); } } systemreports/badges.php 0000604 00000025441 15062573052 0011454 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. declare(strict_types=1); namespace core_badges\reportbuilder\local\systemreports; use core\context\{course, system}; use core_badges\reportbuilder\local\entities\badge; use core_reportbuilder\local\helpers\database; use core_reportbuilder\local\report\{action, column}; use core_reportbuilder\system_report; use lang_string; use moodle_url; use pix_icon; use stdClass; defined('MOODLE_INTERNAL') || die; global $CFG; require_once("{$CFG->libdir}/badgeslib.php"); /** * Badges system report class implementation * * @package core_badges * @copyright 2023 David Carrillo <davidmc@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class badges extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { // Our main entity, it contains all of the column definitions that we need. $badgeentity = new badge(); $entityalias = $badgeentity->get_table_alias('badge'); $this->set_main_table('badge', $entityalias); $this->add_entity($badgeentity); $paramtype = database::generate_param_name(); $context = $this->get_context(); if ($context instanceof system) { $type = BADGE_TYPE_SITE; $this->add_base_condition_sql("{$entityalias}.type = :$paramtype", [$paramtype => $type]); } else { $type = BADGE_TYPE_COURSE; $paramcourseid = database::generate_param_name(); $this->add_base_condition_sql("{$entityalias}.type = :$paramtype AND {$entityalias}.courseid = :$paramcourseid", [$paramtype => $type, $paramcourseid => $context->instanceid]); } // Any columns required by actions should be defined here to ensure they're always available. $this->add_base_fields("{$entityalias}.id, {$entityalias}.type, {$entityalias}.courseid, {$entityalias}.status"); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns($badgeentity); $this->add_filters(); $this->add_actions(); // Set initial sorting by name. $this->set_initial_sort_column('badge:namewithlink', SORT_ASC); // Set if report can be downloaded. $this->set_downloadable(false); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_any_capability([ 'moodle/badges:viewawarded', 'moodle/badges:createbadge', 'moodle/badges:awardbadge', 'moodle/badges:configurecriteria', 'moodle/badges:configuremessages', 'moodle/badges:configuredetails', 'moodle/badges:deletebadge'], $this->get_context()); } /** * Adds the columns we want to display in the report * * They are provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier. If custom columns are needed just for this report, they can be defined here. * * @param badge $badgeentity */ public function add_columns(badge $badgeentity): void { $columns = [ 'badge:image', 'badge:namewithlink', 'badge:status', 'badge:criteria', ]; $this->add_columns_from_entities($columns); // Issued badges column. // TODO: Move this column to the entity when MDL-76392 is integrated. $tempbadgealias = database::generate_alias(); $badgeentityalias = $badgeentity->get_table_alias('badge'); $this->add_column((new column( 'issued', new lang_string('awards', 'core_badges'), $badgeentity->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_INTEGER) ->add_field("(SELECT COUNT({$tempbadgealias}.userid) FROM {badge_issued} {$tempbadgealias} INNER JOIN {user} u ON {$tempbadgealias}.userid = u.id WHERE {$tempbadgealias}.badgeid = {$badgeentityalias}.id AND u.deleted = 0)", 'issued') ->set_is_sortable(true)); // Remove title from image column. $this->get_column('badge:image')->set_title(null); // Change title from namewithlink column. $this->get_column('badge:namewithlink')->set_title(new lang_string('name')); } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $filters = [ 'badge:name', 'badge:status', ]; $this->add_filters_from_entities($filters); } /** * Add the system report actions. An extra column will be appended to each row, containing all actions added here * * Note the use of ":id" placeholder which will be substituted according to actual values in the row */ protected function add_actions(): void { // Activate badge. $this->add_action((new action( new moodle_url('/badges/action.php', [ 'id' => ':id', 'activate' => true, 'return' => ':return', ]), new pix_icon('t/show', '', 'core'), [], false, new lang_string('activate', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); // Populate the return URL. $row->return = (new moodle_url('/badges/index.php', ['type' => $badge->type, 'id' => (int) $badge->courseid]))->out_as_local_url(false); return has_capability('moodle/badges:configuredetails', $badge->get_context()) && $badge->has_criteria() && ($row->status == BADGE_STATUS_INACTIVE || $row->status == BADGE_STATUS_INACTIVE_LOCKED); })); // Deactivate badge. $this->add_action((new action( new moodle_url('/badges/index.php', [ 'lock' => ':id', 'sesskey' => sesskey(), 'type' => ':type', 'id' => ':courseid', ]), new pix_icon('t/hide', '', 'core'), [], false, new lang_string('deactivate', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); return has_capability('moodle/badges:configuredetails', $badge->get_context()) && $badge->has_criteria() && $row->status != BADGE_STATUS_INACTIVE && $row->status != BADGE_STATUS_INACTIVE_LOCKED; })); // Award badge manually. $this->add_action((new action( new moodle_url('/badges/award.php', [ 'id' => ':id', ]), new pix_icon('t/award', '', 'core'), [], false, new lang_string('award', 'badges') ))->add_callback(static function(stdclass $row): bool { $badge = new \core_badges\badge($row->id); return has_capability('moodle/badges:awardbadge', $badge->get_context()) && $badge->has_manual_award_criteria() && $badge->is_active(); })); // Edit action. $this->add_action((new action( new moodle_url('/badges/edit.php', [ 'id' => ':id', 'action' => 'badge', ]), new pix_icon('t/edit', '', 'core'), [], false, new lang_string('edit', 'core') ))->add_callback(static function(stdclass $row): bool { $context = self::get_badge_context((int)$row->type, (int)$row->courseid); return has_capability('moodle/badges:configuredetails', $context); })); // Duplicate action. $this->add_action((new action( new moodle_url('/badges/action.php', [ 'id' => ':id', 'copy' => 1, 'sesskey' => sesskey(), ]), new pix_icon('t/copy', '', 'core'), [], false, new lang_string('copy', 'badges') ))->add_callback(static function(stdclass $row): bool { $context = self::get_badge_context((int)$row->type, (int)$row->courseid); return has_capability('moodle/badges:createbadge', $context); })); // Delete action. $this->add_action((new action( new moodle_url('/badges/index.php', [ 'delete' => ':id', 'type' => ':type', 'id' => ':courseid', ]), new pix_icon('t/delete', '', 'core'), [], false, new lang_string('delete', 'core') ))->add_callback(static function(stdclass $row): bool { $context = self::get_badge_context((int)$row->type, (int)$row->courseid); return has_capability('moodle/badges:deletebadge', $context); })); } /** * Return badge context based on type and courseid * * @param int $type * @param int $courseid * @return \core\context * @throws \coding_exception */ private static function get_badge_context(int $type, int $courseid): \core\context { switch ($type) { case BADGE_TYPE_SITE: return system::instance(); case BADGE_TYPE_COURSE: return course::instance($courseid); default: throw new \coding_exception('Wrong context'); } } /** * CSS classes to add to the row * * @param stdClass $row * @return string */ public function get_row_class(stdClass $row): string { return ($row->status == BADGE_STATUS_INACTIVE_LOCKED || $row->status == BADGE_STATUS_INACTIVE) ? 'text-muted' : ''; } } entities/role.php 0000604 00000012205 15062604107 0010037 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_role\reportbuilder\local\entities; use context; use context_helper; use lang_string; use stdClass; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\filters\select; use core_reportbuilder\local\report\{column, filter}; /** * Role entity * * @package core_role * @copyright 2023 Paul Holden <paulh@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class role extends base { /** * Database tables that this entity uses and their default aliases * * @return array */ protected function get_default_table_aliases(): array { return [ 'context' => 'rctx', 'role' => 'r', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('role'); } /** * Initialise the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } // All the filters defined by the entity can also be used as conditions. $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this ->add_filter($filter) ->add_condition($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { global $DB; $contextalias = $this->get_table_alias('context'); $rolealias = $this->get_table_alias('role'); // Name column. $columns[] = (new column( 'name', new lang_string('rolefullname', 'core_role'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$rolealias}.name, {$rolealias}.shortname, {$rolealias}.id, {$contextalias}.id AS contextid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_is_sortable(true) ->set_callback(static function($name, stdClass $role): string { if ($name === null) { return ''; } context_helper::preload_from_record($role); $context = context::instance_by_id($role->contextid); return role_get_name($role, $context, ROLENAME_BOTH); }); // Short name column. $columns[] = (new column( 'shortname', new lang_string('roleshortname', 'core_role'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$rolealias}.shortname") ->set_is_sortable(true); // Description column. $descriptionfieldsql = "{$rolealias}.description"; if ($DB->get_dbfamily() === 'oracle') { $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); } $columns[] = (new column( 'description', new lang_string('description'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) ->add_field($descriptionfieldsql, 'description') ->add_field("{$rolealias}.shortname") ->set_callback(static function($description, stdClass $role): string { if ($description === null) { return ''; } return role_get_description($role); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $rolealias = $this->get_table_alias('role'); // Name filter. $filters[] = (new filter( select::class, 'name', new lang_string('rolefullname', 'core_role'), $this->get_entity_name(), "{$rolealias}.id" )) ->add_joins($this->get_joins()) ->set_options_callback(static function(): array { return role_get_names(null, ROLENAME_ORIGINAL, true); }); return $filters; } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка