From 6d9dcecb89c8f4f2180066caa932ef5ad9edd80e Mon Sep 17 00:00:00 2001 From: Jacques Monnard Date: Wed, 16 Apr 2025 09:46:38 +0200 Subject: [PATCH 1/2] Update for bootstrap 5 --- templates/action_menu_trigger.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/action_menu_trigger.mustache b/templates/action_menu_trigger.mustache index 5d467d8f..c442b035 100644 --- a/templates/action_menu_trigger.mustache +++ b/templates/action_menu_trigger.mustache @@ -89,7 +89,7 @@ class="{{triggerextraclasses}} dropdown-toggle icon-no-margin" id="action-menu-toggle-{{instance}}" aria-label="{{title}}" - data-toggle="dropdown" + data-bs-toggle="dropdown" role="{{triggerrole}}" aria-haspopup="true" aria-expanded="false" From 55722e1d4e1a5fd2534054c73e43a45db9c9ad20 Mon Sep 17 00:00:00 2001 From: Daniel Neis Araujo Date: Fri, 5 Sep 2025 17:34:21 -0300 Subject: [PATCH 2/2] File upload in message editor, with page to list and delete uploaded images --- amd/build/images_list.min.js | 3 + amd/build/images_list.min.js.map | 1 + amd/src/images_list.js | 92 ++++++++ classes/external.php | 12 +- classes/external/delete_image.php | 89 ++++++++ classes/local/systemreports/images_list.php | 237 ++++++++++++++++++++ db/services.php | 9 + lang/en/scheduler.php | 5 + lib.php | 2 + manageimages.php | 63 ++++++ message_form.php | 2 +- renderer.php | 1 + teacherview.controller.php | 8 +- version.php | 4 +- view.php | 2 + 15 files changed, 520 insertions(+), 10 deletions(-) create mode 100644 amd/build/images_list.min.js create mode 100644 amd/build/images_list.min.js.map create mode 100644 amd/src/images_list.js create mode 100644 classes/external/delete_image.php create mode 100644 classes/local/systemreports/images_list.php create mode 100644 manageimages.php diff --git a/amd/build/images_list.min.js b/amd/build/images_list.min.js new file mode 100644 index 00000000..3516be30 --- /dev/null +++ b/amd/build/images_list.min.js @@ -0,0 +1,3 @@ +define("mod_scheduler/images_list",["exports","core/event_dispatcher","core/notification","core/pending","core/prefetch","core/str","core/toast","core_reportbuilder/local/events","core/ajax"],(function(_exports,_event_dispatcher,_notification,_pending,_prefetch,_str,_toast,reportEvents,_ajax){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.deleteImage=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),reportEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(reportEvents),_ajax=_interopRequireDefault(_ajax);const deleteImage=imageId=>{const request={methodname:"mod_scheduler_delete_image",args:{imageid:imageId}};return _ajax.default.call([request])[0]};_exports.deleteImage=deleteImage;_exports.init=()=>{(0,_prefetch.prefetchStrings)("mod_scheduler",["deleteimage","deleteimageconfirm","imagedeleted"]),(0,_prefetch.prefetchStrings)("core",["delete"]),document.addEventListener("click",(event=>{const imageDelete=event.target.closest('[data-action="image-delete"]');if(imageDelete){event.preventDefault();const triggerElement=imageDelete.closest(".dropdown").querySelector(".dropdown-toggle");_notification.default.saveCancelPromise((0,_str.getString)("deleteimage","mod_scheduler"),(0,_str.getString)("deleteimageconfirm","mod_scheduler",imageDelete.dataset.imageName),(0,_str.getString)("delete","core"),{triggerElement:triggerElement}).then((()=>{const pendingPromise=new _pending.default("mod_scheduler/image:delete"),reportElement=event.target.closest('[data-region="core_reportbuilder/report"]');return deleteImage(imageDelete.dataset.imageId).then((()=>(0,_toast.add)((0,_str.getString)("imagedeleted","mod_scheduler")))).then((()=>((0,_event_dispatcher.dispatchEvent)(reportEvents.tableReload,{preservePagination:!0},reportElement),pendingPromise.resolve()))).catch(_notification.default.exception)})).catch((()=>{}))}}))}})); + +//# sourceMappingURL=images_list.min.js.map \ No newline at end of file diff --git a/amd/build/images_list.min.js.map b/amd/build/images_list.min.js.map new file mode 100644 index 00000000..164f1f34 --- /dev/null +++ b/amd/build/images_list.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"images_list.min.js","sources":["../src/images_list.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Report builder images list management\n *\n * @module core_imagebuilder/images_list\n * @copyright 2021 David Matamoros \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport Ajax from 'core/ajax';\n\n/**\n * Delete given image\n *\n * @param {Number} imageId\n * @return {Promise}\n */\nexport const deleteImage = imageId => {\n const request = {\n methodname: 'mod_scheduler_delete_image',\n args: {imageid: imageId}\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Initialise module\n */\nexport const init = () => {\n prefetchStrings('mod_scheduler', [\n 'deleteimage',\n 'deleteimageconfirm',\n 'imagedeleted',\n ]);\n\n prefetchStrings('core', [\n 'delete',\n ]);\n\n document.addEventListener('click', event => {\n const imageDelete = event.target.closest('[data-action=\"image-delete\"]');\n if (imageDelete) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = imageDelete.closest('.dropdown').querySelector('.dropdown-toggle');\n Notification.saveCancelPromise(\n getString('deleteimage', 'mod_scheduler'),\n getString('deleteimageconfirm', 'mod_scheduler', imageDelete.dataset.imageName),\n getString('delete', 'core'),\n {triggerElement}\n ).then(() => {\n const pendingPromise = new Pending('mod_scheduler/image:delete');\n const reportElement = event.target.closest('[data-region=\"core_reportbuilder/report\"]');\n\n return deleteImage(imageDelete.dataset.imageId)\n .then(() => addToast(getString('imagedeleted', 'mod_scheduler')))\n .then(() => {\n dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement);\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n });\n};\n"],"names":["deleteImage","imageId","request","methodname","args","imageid","Ajax","call","document","addEventListener","event","imageDelete","target","closest","preventDefault","triggerElement","querySelector","saveCancelPromise","dataset","imageName","then","pendingPromise","Pending","reportElement","reportEvents","tableReload","preservePagination","resolve","catch","Notification","exception"],"mappings":"6hDAwCaA,YAAcC,gBACjBC,QAAU,CACZC,WAAY,6BACZC,KAAM,CAACC,QAASJ,iBAGbK,cAAKC,KAAK,CAACL,UAAU,mDAMZ,mCACA,gBAAiB,CAC7B,cACA,qBACA,+CAGY,OAAQ,CACpB,WAGJM,SAASC,iBAAiB,SAASC,cACzBC,YAAcD,MAAME,OAAOC,QAAQ,mCACrCF,YAAa,CACbD,MAAMI,uBAGAC,eAAiBJ,YAAYE,QAAQ,aAAaG,cAAc,0CACzDC,mBACT,kBAAU,cAAe,kBACzB,kBAAU,qBAAsB,gBAAiBN,YAAYO,QAAQC,YACrE,kBAAU,SAAU,QACpB,CAACJ,eAAAA,iBACHK,MAAK,WACGC,eAAiB,IAAIC,iBAAQ,8BAC7BC,cAAgBb,MAAME,OAAOC,QAAQ,oDAEpCb,YAAYW,YAAYO,QAAQjB,SAClCmB,MAAK,KAAM,eAAS,kBAAU,eAAgB,oBAC9CA,MAAK,yCACYI,aAAaC,YAAa,CAACC,oBAAoB,GAAOH,eAC7DF,eAAeM,aAEzBC,MAAMC,sBAAaC,cACzBF,OAAM"} \ No newline at end of file diff --git a/amd/src/images_list.js b/amd/src/images_list.js new file mode 100644 index 00000000..bd1a6594 --- /dev/null +++ b/amd/src/images_list.js @@ -0,0 +1,92 @@ +// 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 . + +/** + * Report builder images list management + * + * @module core_imagebuilder/images_list + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import {dispatchEvent} from 'core/event_dispatcher'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; +import {prefetchStrings} from 'core/prefetch'; +import {getString} from 'core/str'; +import {add as addToast} from 'core/toast'; +import * as reportEvents from 'core_reportbuilder/local/events'; +import Ajax from 'core/ajax'; + +/** + * Delete given image + * + * @param {Number} imageId + * @return {Promise} + */ +export const deleteImage = imageId => { + const request = { + methodname: 'mod_scheduler_delete_image', + args: {imageid: imageId} + }; + + return Ajax.call([request])[0]; +}; + +/** + * Initialise module + */ +export const init = () => { + prefetchStrings('mod_scheduler', [ + 'deleteimage', + 'deleteimageconfirm', + 'imagedeleted', + ]); + + prefetchStrings('core', [ + 'delete', + ]); + + document.addEventListener('click', event => { + const imageDelete = event.target.closest('[data-action="image-delete"]'); + if (imageDelete) { + event.preventDefault(); + + // Use triggerElement to return focus to the action menu toggle. + const triggerElement = imageDelete.closest('.dropdown').querySelector('.dropdown-toggle'); + Notification.saveCancelPromise( + getString('deleteimage', 'mod_scheduler'), + getString('deleteimageconfirm', 'mod_scheduler', imageDelete.dataset.imageName), + getString('delete', 'core'), + {triggerElement} + ).then(() => { + const pendingPromise = new Pending('mod_scheduler/image:delete'); + const reportElement = event.target.closest('[data-region="core_reportbuilder/report"]'); + + return deleteImage(imageDelete.dataset.imageId) + .then(() => addToast(getString('imagedeleted', 'mod_scheduler'))) + .then(() => { + dispatchEvent(reportEvents.tableReload, {preservePagination: true}, reportElement); + return pendingPromise.resolve(); + }) + .catch(Notification.exception); + }).catch(() => { + return; + }); + } + }); +}; diff --git a/classes/external.php b/classes/external.php index 3c8ab315..47adeea5 100644 --- a/classes/external.php +++ b/classes/external.php @@ -28,13 +28,13 @@ require_once("$CFG->libdir/externallib.php"); -use external_api; -use external_function_parameters; -use external_value; -use external_single_structure; -use external_multiple_structure; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\external_single_structure; +use core_external\external_multiple_structure; -use \mod_scheduler\model\scheduler; +use mod_scheduler\model\scheduler; /** * This is the external API for this component. diff --git a/classes/external/delete_image.php b/classes/external/delete_image.php new file mode 100644 index 00000000..b86249da --- /dev/null +++ b/classes/external/delete_image.php @@ -0,0 +1,89 @@ +. + +/** + * This is the external API for this component. + * + * @package mod_scheduler + * @copyright 2022 University of Glasgow + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_scheduler\external; + +defined('MOODLE_INTERNAL') || die(); + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use core_external\external_single_structure; +use core_external\external_multiple_structure; + +use \mod_scheduler\model\scheduler; + +/** + * This is the external API for this component. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_image extends external_api { + + /** + * Delete image parameters + * + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'imageid' => new external_value(PARAM_INT, 'The image id', VALUE_REQUIRED), + ]); + } + + public static function execute(int $imageid): bool { + global $DB; + + [ + 'imageid' => $imageid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'imageid' => $imageid, + ]); + + $filerecord = $DB->get_record('files', ['id' => $imageid]); + if ($filerecord->component !== 'mod_scheduler') { + return false; + } + if ($filerecord->filearea !== 'message') { + return false; + } + $ctx = \context::instance_by_id($filerecord->contextid); + if (!has_any_capability(['mod/scheduler:manage', 'mod/scheduler:manageallappointments'], $ctx)) { + return false; + } + $fs = get_file_storage(); + $file = $fs->get_file($filerecord->contextid, 'mod_scheduler', 'message', $filerecord->itemid, $filerecord->filepath, $filerecord->filename); + $file->delete(); + return true; + } + + /** + * External method return value + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, 'Success'); + } +} diff --git a/classes/local/systemreports/images_list.php b/classes/local/systemreports/images_list.php new file mode 100644 index 00000000..802157fa --- /dev/null +++ b/classes/local/systemreports/images_list.php @@ -0,0 +1,237 @@ +. + +declare(strict_types=1); + +namespace mod_scheduler\local\systemreports; + +use html_writer; +use lang_string; +use moodle_url; +use pix_icon; +use stdClass; +use core_reportbuilder\datasource; +use core_reportbuilder\manager; +use core_reportbuilder\system_report; +use core_reportbuilder\local\entities\user; +use core_reportbuilder\local\filters\{boolean_select, date, tags, text, select}; +use core_reportbuilder\local\helpers\{audience, custom_fields, format}; +use core_reportbuilder\local\report\{action, column, filter}; +use core_reportbuilder\output\report_name_editable; +use core_reportbuilder\local\models\report; +use core_reportbuilder\permission; +use core_tag\reportbuilder\local\entities\tag; +use core_tag_tag; + +/** + * Images list + * + * @package mod_scheduler + * @copyright Daniel Neis Araujo + * @copyright Based on reports lists from 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class images_list extends system_report { + + /** + * The name of our internal report entity + * + * @return string + */ + private function get_report_entity_name(): string { + return 'report'; + } + + /** + * Initialise the report + */ + protected function initialise(): void { + $this->set_main_table('files', 'f'); + $report = $this->get_report_persistent(); + $this->add_base_condition_simple('f.itemid', $report->get('itemid')); + $this->add_base_condition_simple('f.filearea', $report->get('area')); + $this->add_base_condition_simple('f.component', $report->get('component')); + $this->add_base_condition_simple('f.contextid', $report->get('contextid')); + $this->add_base_condition_sql('f.filename != "."'); + + // Select fields required for actions, permission checks, and row class callbacks. + $this->add_base_fields('f.id, f.contenthash, f.filename, f.filearea'); + + // Join user entity for "Userid" column. + $entityuser = new user(); + $entityuseralias = $entityuser->get_table_alias('user'); + $this->add_entity($entityuser + ->add_join("JOIN {user} {$entityuseralias} ON {$entityuseralias}.id = f.userid") + ); + + // Define our internal entity for report elements. + $this->annotate_entity($this->get_report_entity_name(), + new lang_string('imageslist', 'mod_scheduler')); + + $this->add_columns(); + $this->add_filters(); + $this->add_actions(); + + $this->set_downloadable(false); + } + + /** + * Ensure we can view the report + * + * @return bool + */ + protected function can_view(): bool { + // TODO: testar capabilities do modulo + return permission::can_view_reports_list(); + } + + /** + * Dim the table row for invalid datasource + * + * @param stdClass $row + * @return string + public function get_row_class(stdClass $row): string { + return $this->report_source_valid($row->source) ? '' : 'text-muted'; + } + */ + + protected function get_file_url($value) { + global $CFG; + $report = $this->get_report_persistent(); + return $CFG->wwwroot . '/pluginfile.php/' . + $report->get('contextid') . '/mod_scheduler/message/' . $report->get('itemid') . '/' . $value; + + } + + /** + * Add columns to report + */ + protected function add_columns(): void { + $tablealias = $this->get_main_table_alias(); + + // Report name column. + $this->add_column((new column( + 'filename', + new lang_string('file'), + $this->get_report_entity_name() + )) + ->set_type(column::TYPE_TEXT) + // We need enough fields to re-create the persistent and pass to the editable component. + ->add_fields("{$tablealias}.filename") + ->set_is_sortable(true, ["{$tablealias}.filename"]) + ->add_callback(function(string $value, stdClass $row, $report) { + $url = $this->get_file_url($value); + return html_writer::link($url, $value); + }) + ); + + // Time created column. + $this->add_column((new column( + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_report_entity_name() + )) + ->set_type(column::TYPE_TIMESTAMP) + ->add_fields("{$tablealias}.timecreated") + ->set_is_sortable(true) + ->add_callback([format::class, 'userdate']) + ); + + // The user who modified the report. + $this->add_column_from_entity('user:fullnamewithlink') + ->set_title(new lang_string('user')); + + // Initial sorting. + $this->set_initial_sort_column('report:timecreated', SORT_DESC); + } + + /** + * Add filters to report + */ + protected function add_filters(): void { + $tablealias = $this->get_main_table_alias(); + + // Name filter. + $this->add_filter((new filter( + text::class, + 'filename', + new lang_string('file'), + $this->get_report_entity_name(), + "{$tablealias}.filename" + ))); + + // Time created filter. + $this->add_filter((new filter( + date::class, + 'timecreated', + new lang_string('timecreated', 'core_reportbuilder'), + $this->get_report_entity_name(), + "{$tablealias}.timecreated" + )) + ->set_limited_operators([ + date::DATE_ANY, + date::DATE_RANGE, + date::DATE_BEFORE, + date::DATE_LAST, + date::DATE_CURRENT, + ]) + ); + + // User modified filter. + $this->add_filter_from_entity('user:userselect') + ->set_header(new lang_string('usermodified', 'reportbuilder')) + ->set_is_available(has_capability('moodle/user:viewalldetails', $this->get_context())); + } + + /** + * Add actions to report + */ + protected function add_actions(): void { + // Delete action. + $this->add_action((new action( + new moodle_url('#'), + new pix_icon('t/delete', ''), + [ + 'data-action' => 'image-delete', + 'data-image-id' => ':id', + 'data-image-name' => ':name', + 'class' => 'text-danger', + ], + false, + new lang_string('deleteimage', 'mod_scheduler') + )) + ->add_callback(function(stdClass $row): bool { + + // Ensure data name attribute is properly formatted. + $report = new report(0, $row); + $row->name = $report->get_formatted_name(); + + // We don't check whether report is valid to ensure editor can always delete them. + return permission::can_edit_report($report); + }) + ); + } + + /** + * Helper to determine whether given report source is valid (it both exists, and is available) + * + * @param string $source + * @return bool + */ + private function report_source_valid(string $source): bool { + return manager::report_source_exists($source, datasource::class) && manager::report_source_available($source); + } +} diff --git a/db/services.php b/db/services.php index 6faa22da..313bcea0 100644 --- a/db/services.php +++ b/db/services.php @@ -34,4 +34,13 @@ 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + + 'mod_scheduler_delete_image' => array( + 'classname' => 'mod_scheduler\external\delete_image', + 'classpath' => 'mod/scheduler/classes/external/delete_image', + 'methodname' => 'execute', + 'description' => 'Delete a given image.', + 'type' => 'write', + 'ajax' => true, + ), ); diff --git a/lang/en/scheduler.php b/lang/en/scheduler.php index 3b2b19db..8317d412 100644 --- a/lang/en/scheduler.php +++ b/lang/en/scheduler.php @@ -203,6 +203,8 @@ $string['deleteallslots'] = 'Delete all slots'; $string['deleteallunusedslots'] = 'Delete unused slots'; $string['deletecommands'] = 'Delete slots'; +$string['deleteimage'] = 'Delete image'; +$string['deleteimageconfirm'] = 'Are you sure you want to delete the image? It cannot be undone.'; $string['deletemyslots'] = 'Delete all my slots'; $string['deleteselection'] = 'Delete selected slots'; $string['deletetheseslots'] = 'Delete these slots'; @@ -302,12 +304,15 @@ $string['incourse'] = ' in course '; $string['mixindivgroup'] = 'Mix individual and group bookings'; $string['mixindivgroup_desc'] = 'Where group scheduling is enabled, allow individual bookings as well.'; +$string['imageslist'] = 'Images list'; +$string['imagedeleted'] = 'Image deleted'; $string['introduction'] = 'Introduction'; $string['isnonexclusive'] = 'Non-exclusive'; $string['landscape'] = 'Landscape'; $string['lengthbreakdown'] = 'By slot duration'; $string['limited'] = 'Limited ({$a} left)'; $string['location'] = 'Location'; +$string['manageimages'] = 'Manage images'; $string['markseen'] = 'After you have had an appointment with a student please mark them as "Seen" by clicking the checkbox near to their user picture above.'; $string['markasseennow'] = 'Mark as seen now'; $string['maxgrade'] = 'Take the highest grade'; diff --git a/lib.php b/lib.php index 14119c92..fc5e15e8 100644 --- a/lib.php +++ b/lib.php @@ -719,6 +719,8 @@ function scheduler_pluginfile($course, $cm, $context, $filearea, $args, $forcedo $permissions->ensure($permissions->can_see_appointment($app)); + } else if ($filearea === 'message') { + // TODO. } else { // Unknown file area. return false; diff --git a/manageimages.php b/manageimages.php new file mode 100644 index 00000000..f27b3595 --- /dev/null +++ b/manageimages.php @@ -0,0 +1,63 @@ +. + +/** + * Shows a sortable list of appointments + * + * @package mod_scheduler + * @copyright 2015 Henning Bostelmann and others (see README.txt) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use mod_scheduler\local\systemreports\images_list; + +$PAGE->set_docs_path('mod/scheduler/manageimages'); + +$scopecontext = context_course::instance($scheduler->courseid); + +// TODO: test capability + +$taburl = new moodle_url('/mod/scheduler/view.php', + array('id' => $scheduler->cmid, 'what' => 'manageimages')); +$returnurl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid)); + +$PAGE->set_url($taburl); + +$PAGE->requires->js_call_amd('mod_scheduler/images_list', 'init'); + +echo $output->header(); + +// Print top tabs. + +echo $output->teacherview_tabs($scheduler, $permissions, $taburl, 'manageimages'); + +// Find active group in case that group mode is in use. +$currentgroupid = 0; +$groupmode = groups_get_activity_groupmode($scheduler->cm); +if ($groupmode) { + $currentgroupid = groups_get_activity_group($scheduler->cm, true); + + echo html_writer::start_div('dropdownmenu'); + groups_print_activity_menu($scheduler->cm, $taburl); + echo html_writer::end_div(); +} + +$report = core_reportbuilder\system_report_factory::create(images_list::class, context_module::instance($scheduler->cmid), 'mod_scheduler', 'message', $scheduler->cmid); +echo $report->output(); + +echo $output->footer(); diff --git a/message_form.php b/message_form.php index db55ea83..c66a9391 100644 --- a/message_form.php +++ b/message_form.php @@ -92,7 +92,7 @@ protected function definition() { } $bodyedit = $mform->addElement('editor', 'body', get_string('messagebody', 'scheduler'), - array('rows' => 15, 'columns' => 60), array('collapsed' => true)); + array('rows' => 15, 'columns' => 60), array('collapsed' => true,'maxfiles' => EDITOR_UNLIMITED_FILES)); $mform->setType('body', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. if (isset($this->_customdata['body'])) { $bodyedit->setValue(array('text' => $this->_customdata['body'])); diff --git a/renderer.php b/renderer.php index e5e53f3e..ccd556ac 100644 --- a/renderer.php +++ b/renderer.php @@ -370,6 +370,7 @@ public function teacherview_tabs(scheduler $scheduler, scheduler_permissions $pe $level1[] = $this->teacherview_tab($baseurl, 'datelist', 'datelist'); $level1[] = $statstab; $level1[] = $this->teacherview_tab($baseurl, 'export', 'export'); + $level1[] = $this->teacherview_tab($baseurl, 'manageimages', 'manageimages'); return $this->tabtree($level1, $selected, $inactive); } diff --git a/teacherview.controller.php b/teacherview.controller.php index 6cdb092e..70b78739 100644 --- a/teacherview.controller.php +++ b/teacherview.controller.php @@ -155,11 +155,17 @@ function scheduler_action_dosendmessage($scheduler, $formdata, $returnurl) { $data = (object) $formdata; + $draftitemid = file_get_submitted_draft_itemid('body'); + // TODO: link file to notifications instead of course module? + $rawmessage = file_save_draft_area_files($draftitemid, $scheduler->context->id, + 'mod_scheduler', 'message', $scheduler->cm->id, + array('subdirs'=>false, 'maxfiles' => -1), $data->body['text']); + $rawmessage = file_rewrite_pluginfile_urls($rawmessage, 'pluginfile.php', $scheduler->context->id, 'mod_scheduler', 'message', $scheduler->cm->id); + $recipients = $data->recipient; if ($data->copytomyself) { $recipients[$USER->id] = 1; } - $rawmessage = $data->body['text']; $format = $data->body['format']; $textmessage = format_text_email($rawmessage, $format); $htmlmessage = null; diff --git a/version.php b/version.php index 4952256a..2c129227 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ */ $plugin->component = 'mod_scheduler'; // Full name of the plugin (used for diagnostics). -$plugin->version = 2024080103; // The current module version (Date: YYYYMMDDXX). -$plugin->release = '4.x dev'; // Human-friendly version name. +$plugin->version = 2025091101; // The current module version (Date: YYYYMMDDXX). +$plugin->release = '5.x dev'; // Human-friendly version name. $plugin->requires = 2023100905; // Requires Moodle 4.3. $plugin->maturity = MATURITY_ALPHA; // Development release - not for production use. diff --git a/view.php b/view.php index 1edfed23..e4e3c12f 100644 --- a/view.php +++ b/view.php @@ -84,6 +84,8 @@ include($CFG->dirroot.'/mod/scheduler/export.php'); } else if ($action == 'datelist') { include($CFG->dirroot.'/mod/scheduler/datelist.php'); + } else if ($action == 'manageimages') { + include($CFG->dirroot.'/mod/scheduler/manageimages.php'); } else { include($CFG->dirroot.'/mod/scheduler/teacherview.php'); }