diff --git a/.upgradenotes/MDL-82322-2025073102510933.yml b/.upgradenotes/MDL-82322-2025073102510933.yml new file mode 100644 index 00000000000..e35dfc77062 --- /dev/null +++ b/.upgradenotes/MDL-82322-2025073102510933.yml @@ -0,0 +1,11 @@ +issueNumber: MDL-82322 +notes: + core_course: + - message: | + The following classes have been renamed and now support autoloading. + Existing classes are currently unaffected. + + | Old class name | New class name | + | --- | --- | + | `\course_request` | `\core_course\course_request` | + type: improved diff --git a/public/admin/tool/moodlenet/classes/local/import_handler_registry.php b/public/admin/tool/moodlenet/classes/local/import_handler_registry.php index b5860b5dbba..c0e766aacc5 100644 --- a/public/admin/tool/moodlenet/classes/local/import_handler_registry.php +++ b/public/admin/tool/moodlenet/classes/local/import_handler_registry.php @@ -22,6 +22,8 @@ */ namespace tool_moodlenet\local; +use core_course\dndupload_handler; + /** * The import_handler_registry class. * @@ -113,9 +115,7 @@ class import_handler_registry { protected function populate_handlers() { // Generate a dndupload_handler object, just so we can call ->is_known_type() on the types being registered by plugins. // We must vet each type which is reported to be handled against the list of known, supported types. - global $CFG; - require_once($CFG->dirroot . '/course/dnduploadlib.php'); - $dndhandlers = new \dndupload_handler($this->course); + $dndhandlers = new dndupload_handler($this->course); // Get the list of mods enabled at site level first. We need to cross check this. $pluginman = \core_plugin_manager::instance(); @@ -185,4 +185,3 @@ class import_handler_registry { $this->filehandlers[$extension][] = ['module' => $module, 'message' => $message]; } } - diff --git a/public/course/classes/category.php b/public/course/classes/category.php index 592c1017842..c0b399c9525 100644 --- a/public/course/classes/category.php +++ b/public/course/classes/category.php @@ -24,6 +24,7 @@ */ use core\exception\moodle_exception; +use core_course\course_request; /** * Class to store, cache, render and manage course category @@ -3117,9 +3118,6 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr * @return bool */ public function can_request_course() { - global $CFG; - require_once($CFG->dirroot . '/course/lib.php'); - return course_request::can_request($this->get_context()); } diff --git a/public/course/classes/course_request.php b/public/course/classes/course_request.php new file mode 100644 index 00000000000..ecef8037863 --- /dev/null +++ b/public/course/classes/course_request.php @@ -0,0 +1,467 @@ +. + +namespace core_course; + +use core\context; +use core\context\course as context_course; +use core\context\coursecat as context_coursecat; +use core\context\system as context_system; +use core\exception\coding_exception; +use core\exception\moodle_exception; +use core_course_category; +use restore_dbops; +use stdClass; + +/** + * This class pertains to course requests and contains methods associated with + * create, approving, and removing course requests. + * + * Please note we do not allow embedded images here because there is no context + * to store them with proper access control. + * + * @package core_course + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * + * @property-read int $id + * @property-read string $fullname + * @property-read string $shortname + * @property-read string $summary + * @property-read int $summaryformat + * @property-read int $summarytrust + * @property-read string $reason + * @property-read int $requester + */ +class course_request { + /** + * This is the stdClass that stores the properties for the course request + * and is externally accessed through the __get magic method + * @var stdClass + */ + protected $properties; + + /** + * An array of options for the summary editor used by course request forms. + * + * This is initially set by {@see summary_editor_options()} + * @var array + */ + protected static $summaryeditoroptions; + + /** + * Static function to prepare the summary editor for working with a course request. + * + * @param null|stdClass $data Optional, an object containing the default values + * for the form, these may be modified when preparing the + * editor so this should be called before creating the form + * @return stdClass An object that can be used to set the default values for + * an mforms form + */ + public static function prepare($data = null) { + if ($data === null) { + $data = new stdClass(); + } + $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options()); + return $data; + } + + /** + * Static function to create a new course request when passed an array of properties + * for it. + * + * This function also handles saving any files that may have been used in the editor + * + * @param stdClass $data + * @return course_request The newly created course request + */ + public static function create($data) { + global $USER, $DB, $CFG; + $data->requester = $USER->id; + + // Setting the default category if none set. + if (empty($data->category) || !empty($CFG->lockrequestcategory)) { + $data->category = $CFG->defaultrequestcategory; + } + + // Summary is a required field so copy the text over. + $data->summary = $data->summary_editor['text']; + $data->summaryformat = $data->summary_editor['format']; + + $data->id = $DB->insert_record('course_request', $data); + + // Create a new course_request object and return it. + $request = new course_request($data); + + // Notify the admin if required. + if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) { + $a = new stdClass(); + $a->link = "$CFG->wwwroot/course/pending.php"; + $a->user = fullname($USER); + $a->shortname = s($data->shortname) ?? ''; + $a->fullname = s($data->fullname) ?? ''; + $a->category = s($data->category) ?? ''; + $a->reason = format_text($data->reason, FORMAT_PLAIN) ?? ''; + $subject = get_string('courserequest'); + $message = get_string('courserequestnotifyemail', 'admin', $a); + foreach ($users as $user) { + $request->notify($user, $USER, 'courserequested', $subject, $message); + } + } + + return $request; + } + + /** + * Returns an array of options to use with a summary editor + * + * @uses course_request::$summaryeditoroptions + * @return array An array of options to use with the editor + */ + public static function summary_editor_options() { + global $CFG; + if (self::$summaryeditoroptions === null) { + self::$summaryeditoroptions = ['maxfiles' => 0, 'maxbytes' => 0]; + } + return self::$summaryeditoroptions; + } + + /** + * Loads the properties for this course request object. Id is required and if + * only id is provided then we load the rest of the properties from the database + * + * @param stdClass|int $properties Either an object containing properties + * or the course_request id to load + */ + public function __construct($properties) { + global $DB; + if (empty($properties->id)) { + if (empty($properties)) { + throw new coding_exception('You must provide a course request id when creating a course_request object'); + } + $id = $properties; + $properties = new stdClass(); + $properties->id = (int)$id; + unset($id); + } + if (empty($properties->requester)) { + if (!($this->properties = $DB->get_record('course_request', ['id' => $properties->id]))) { + throw new moodle_exception('unknowncourserequest'); + } + } else { + $this->properties = $properties; + } + $this->properties->collision = null; + } + + /** + * Returns the requested property + * + * @param string $key + * @return mixed + */ + public function __get($key) { + return $this->properties->$key; + } + + /** + * Override this to ensure empty($request->blah) calls return a reliable answer... + * + * This is required because we define the __get method + * + * @param mixed $key + * @return bool True is it not empty, false otherwise + */ + public function __isset($key) { + return (!empty($this->properties->$key)); + } + + /** + * Returns the user who requested this course. + * + * Uses a static var to cache the results and cut down the number of db queries + * + * @return stdClass The user who requested the course + */ + public function get_requester() { + global $DB; + static $requesters = []; + if (!array_key_exists($this->properties->requester, $requesters)) { + $requesters[$this->properties->requester] = $DB->get_record('user', ['id' => $this->properties->requester]); + } + return $requesters[$this->properties->requester]; + } + + /** + * Checks that the shortname used by the course does not conflict with any other + * courses that exist + * + * @param string|null $shortnamemark The string to append to the requests shortname + * should a conflict be found + * @return bool true is there is a conflict, false otherwise + */ + public function check_shortname_collision($shortnamemark = '[*]') { + global $DB; + + if ($this->properties->collision !== null) { + return $this->properties->collision; + } + + if (empty($this->properties->shortname)) { + debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER); + $this->properties->collision = false; + } else if ($DB->record_exists('course', ['shortname' => $this->properties->shortname])) { + if (!empty($shortnamemark)) { + $this->properties->shortname .= ' ' . $shortnamemark; + } + $this->properties->collision = true; + } else { + $this->properties->collision = false; + } + return $this->properties->collision; + } + + /** + * Checks user capability to approve a requested course + * + * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is + * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'. + * + * @return bool + */ + public function can_approve() { + global $CFG; + $category = null; + if ($this->properties->category) { + $category = core_course_category::get($this->properties->category, IGNORE_MISSING); + } else if ($CFG->defaultrequestcategory) { + $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING); + } + if ($category) { + return has_capability('moodle/site:approvecourse', $category->get_context()); + } + + // We can not determine the context where the course should be created. The approver should have + // both capabilities to approve courses and change course category in the system context. + return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance()); + } + + /** + * Returns the category where this course request should be created + * + * Note that we don't check here that user has a capability to view + * hidden categories if he has capabilities 'moodle/site:approvecourse' and + * 'moodle/course:changecategory' + * + * @return core_course_category + */ + public function get_category() { + global $CFG; + if ( + $this->properties->category + && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING)) + ) { + return $category; + } else if ( + $CFG->defaultrequestcategory + && ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING)) + ) { + return $category; + } else { + return core_course_category::get_default(); + } + } + + /** + * This function approves the request turning it into a course + * + * This function converts the course request into a course, at the same time + * transferring any files used in the summary to the new course and then removing + * the course request and the files associated with it. + * + * @return int The id of the course that was created from this request + */ + public function approve() { + global $CFG, $DB, $USER; + + require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + + $user = $DB->get_record('user', ['id' => $this->properties->requester, 'deleted' => 0], '*', MUST_EXIST); + + $courseconfig = get_config('moodlecourse'); + + // Transfer appropriate settings. + $data = clone($this->properties); + unset($data->id); + unset($data->reason); + unset($data->requester); + + // Set category. + $category = $this->get_category(); + $data->category = $category->id; + + // Set misc settings. + $data->requested = 1; + + // Apply course default settings. + $data->format = $courseconfig->format; + $data->newsitems = $courseconfig->newsitems; + $data->showgrades = $courseconfig->showgrades; + $data->showreports = $courseconfig->showreports; + $data->maxbytes = $courseconfig->maxbytes; + $data->groupmode = $courseconfig->groupmode; + $data->groupmodeforce = $courseconfig->groupmodeforce; + $data->visible = $courseconfig->visible; + $data->visibleold = $data->visible; + $data->lang = $courseconfig->lang; + $data->enablecompletion = $courseconfig->enablecompletion; + $data->numsections = $courseconfig->numsections; + $data->startdate = usergetmidnight(time()); + if ($courseconfig->courseenddateenabled) { + $data->enddate = usergetmidnight(time()) + $courseconfig->courseduration; + } + + [$data->fullname, $data->shortname] = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname); + + $course = create_course($data); + $context = context_course::instance($course->id, MUST_EXIST); + + // Add enrol instances. + if (!$DB->record_exists('enrol', ['courseid' => $course->id, 'enrol' => 'manual'])) { + if ($manual = enrol_get_plugin('manual')) { + $manual->add_default_instance($course); + } + } + + // Enrol the requester as teacher if necessary. + if ( + !empty($CFG->creatornewroleid) + && !is_viewing($context, $user, 'moodle/role:assign') + && !is_enrolled($context, $user, 'moodle/role:assign') + ) { + enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid); + } + + $this->delete(); + + $a = new stdClass(); + $a->name = format_string($course->fullname, true, ['context' => $context]); + $a->url = course_get_url($course); + + $usernameplaceholders = \core\user::get_name_placeholders($user); + foreach ($usernameplaceholders as $field => $value) { + $a->{$field} = $value; + } + + $this->notify( + touser: $user, + fromuser: $USER, + name: 'courserequestapproved', + subject: get_string('courseapprovedsubject'), + message: get_string('courseapprovedemail2', 'moodle', $a), + courseid: $course->id, + ); + + return $course->id; + } + + /** + * Reject a course request + * + * This function rejects a course request, emailing the requesting user the + * provided notice and then removing the request from the database + * + * @param string $notice The message to display to the user + */ + public function reject($notice) { + global $USER, $DB; + $user = $DB->get_record('user', ['id' => $this->properties->requester], '*', MUST_EXIST); + $this->notify( + touser: $user, + fromuser: $USER, + name: 'courserequestrejected', + subject: get_string('courserejectsubject'), + message: get_string('courserejectemail', 'moodle', $notice), + ); + $this->delete(); + } + + /** + * Deletes the course request and any associated files + */ + public function delete() { + global $DB; + $DB->delete_records('course_request', ['id' => $this->properties->id]); + } + + /** + * Send a message from one user to another using events_trigger + * + * @param object $touser + * @param object $fromuser + * @param string $name + * @param string $subject + * @param string $message + * @param int|null $courseid + */ + protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) { + $eventdata = new \core\message\message(); + $eventdata->courseid = empty($courseid) ? SITEID : $courseid; + $eventdata->component = 'moodle'; + $eventdata->name = $name; + $eventdata->userfrom = $fromuser; + $eventdata->userto = $touser; + $eventdata->subject = $subject; + $eventdata->fullmessage = $message; + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = ''; + $eventdata->smallmessage = ''; + $eventdata->notification = 1; + message_send($eventdata); + } + + /** + * Checks if current user can request a course in this context + * + * @param context $context + * @return bool + */ + public static function can_request(context $context) { + global $CFG; + if (empty($CFG->enablecourserequests)) { + return false; + } + if (has_capability('moodle/course:create', $context)) { + return false; + } + + if ($context instanceof context_system) { + $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING); + return $defaultcontext && has_capability('moodle/course:request', $defaultcontext); + } else if ($context instanceof context_coursecat) { + if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) { + return has_capability('moodle/course:request', $context); + } + } + return false; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(course_request::class, \course_request::class); diff --git a/public/course/classes/dndupload_ajax_processor.php b/public/course/classes/dndupload_ajax_processor.php new file mode 100644 index 00000000000..a36526c986c --- /dev/null +++ b/public/course/classes/dndupload_ajax_processor.php @@ -0,0 +1,340 @@ +. + +namespace core_course; + +use cm_info; +use core\context\course as context_course; +use core\exception\coding_exception; +use core\exception\moodle_exception; +use core_text; +use course_modinfo; +use navigation_cache; +use repository; +use stdClass; + +/** + * Processes the upload, creating the course module and returning the result + * + * @package core + * @copyright 2012 Davo Smith + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dndupload_ajax_processor { + /** Returned when no error has occurred */ + public const ERROR_OK = 0; + + /** @var object The course that we are uploading to */ + protected $course = null; + + /** @var context_course The course context for capability checking */ + protected $context = null; + + /** @var int The section number we are uploading to */ + protected $section = null; + + /** @var string The type of upload (e.g. 'Files', 'text/plain') */ + protected $type = null; + + /** @var object The details of the module type that will be created */ + protected $module = null; + + /** @var object The course module that has been created */ + protected $cm = null; + + /** @var dndupload_handler used to check the allowed file types */ + protected $dnduploadhandler = null; + + /** @var string The name to give the new activity instance */ + protected $displayname = null; + + /** + * Set up some basic information needed to handle the upload + * + * @param int $courseid The ID of the course we are uploading to + * @param int $section The section number we are uploading to + * @param string $type The type of upload (as reported by the browser) + * @param string $modulename The name of the module requested to handle this upload + */ + public function __construct($courseid, $section, $type, $modulename) { + global $DB; + + if (!defined('AJAX_SCRIPT')) { + throw new coding_exception('dndupload_ajax_processor should only be used within AJAX requests'); + } + + $this->course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST); + + require_login($this->course, false); + $this->context = context_course::instance($this->course->id); + + if (!is_number($section) || $section < 0) { + throw new coding_exception("Invalid section number $section"); + } + $this->section = $section; + $this->type = $type; + + if (!$this->module = $DB->get_record('modules', ['name' => $modulename])) { + throw new coding_exception("Module $modulename does not exist"); + } + + $this->dnduploadhandler = new dndupload_handler($this->course); + } + + /** + * Check if this upload is a 'file' upload + * + * @return bool true if it is a 'file' upload, false otherwise + */ + protected function is_file_upload() { + return ($this->type == 'Files'); + } + + /** + * Process the upload - creating the module in the course and returning the result to the browser + * + * @param string $displayname optional the name (from the browser) to give the course module instance + * @param string $content optional the content of the upload (for non-file uploads) + */ + public function process($displayname = null, $content = null) { + require_capability('moodle/course:manageactivities', $this->context); + + if ($this->is_file_upload()) { + require_capability('moodle/course:managefiles', $this->context); + if ($content != null) { + throw new moodle_exception('fileuploadwithcontent', 'moodle'); + } + } else { + if (empty($content)) { + throw new moodle_exception('dnduploadwithoutcontent', 'moodle'); + } + } + + require_sesskey(); + + $this->displayname = $displayname; + + if ($this->is_file_upload()) { + $this->handle_file_upload(); + } else { + $this->handle_other_upload($content); + } + } + + /** + * Handle uploads containing files - create the course module, ask the upload repository + * to process the file, ask the mod to set itself up, then return the result to the browser + */ + protected function handle_file_upload() { + global $CFG; + + // Add the file to a draft file area. + $draftitemid = file_get_unused_draft_itemid(); + $maxbytes = get_user_max_upload_file_size($this->context, $CFG->maxbytes, $this->course->maxbytes); + $types = $this->dnduploadhandler->get_handled_file_types($this->module->name); + $repo = repository::get_instances(['type' => 'upload', 'currentcontext' => $this->context]); + if (empty($repo)) { + throw new moodle_exception('errornouploadrepo', 'moodle'); + } + $repo = reset($repo); // Get the first (and only) upload repo. + // Pre-emptively purge the navigation cache so the upload repo can close the session. + navigation_cache::destroy_volatile_caches(); + $details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid); + if (empty($this->displayname)) { + $this->displayname = $this->display_name_from_file($details['file']); + } + + // Create a course module to hold the new instance. + $this->create_course_module(); + + // Ask the module to set itself up. + $moduledata = $this->prepare_module_data($draftitemid); + $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', [$moduledata], 'invalidfunction'); + if ($instanceid === 'invalidfunction') { + throw new coding_exception(sprintf( + "%s does not support drag and drop upload (missing %s_dndupload_handle function", + $this->module->name, + $this->module->name, + )); + } + + // Finish setting up the course module. + $this->finish_setup_course_module($instanceid); + } + + /** + * Handle uploads not containing file - create the course module, ask the mod to + * set itself up, then return the result to the browser + * + * @param string $content the content uploaded to the browser + */ + protected function handle_other_upload($content) { + // Check this plugin is registered to handle this type of upload. + if (!$this->dnduploadhandler->has_type_handler($this->module->name, $this->type)) { + $info = (object)['modname' => $this->module->name, 'type' => $this->type]; + throw new moodle_exception('moddoesnotsupporttype', 'moodle', $info); + } + + // Create a course module to hold the new instance. + $this->create_course_module(); + + // Ask the module to set itself up. + $moduledata = $this->prepare_module_data(null, $content); + $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', [$moduledata], 'invalidfunction'); + if ($instanceid === 'invalidfunction') { + throw new coding_exception(sprintf( + "%s does not support drag and drop upload (missing %s_dndupload_handle function", + $this->module->name, + $this->module->name, + )); + } + + // Finish setting up the course module. + $this->finish_setup_course_module($instanceid); + } + + /** + * Generate the name of the mod instance from the name of the file + * (remove the extension and convert underscore => space + * + * @param string $filename the filename of the uploaded file + * @return string the display name to use + */ + protected function display_name_from_file($filename) { + $pos = core_text::strrpos($filename, '.'); + if ($pos) { // Want to skip if $pos === 0 OR $pos === false. + $filename = core_text::substr($filename, 0, $pos); + } + return str_replace('_', ' ', $filename); + } + + /** + * Create the coursemodule to hold the file/content that has been uploaded + */ + protected function create_course_module() { + global $CFG; + require_once($CFG->dirroot . '/course/modlib.php'); + [$module, $context, $cw, $cm, $data] = prepare_new_moduleinfo_data($this->course, $this->module->name, $this->section); + + $data->coursemodule = $data->id = add_course_module($data); + $this->cm = $data; + } + + /** + * Gather together all the details to pass on to the mod, so that it can initialise it's + * own database tables + * + * @param int $draftitemid optional the id of the draft area containing the file (for file uploads) + * @param string $content optional the content dropped onto the course (for non-file uploads) + * @return object data to pass on to the mod, containing: + * string $type the 'type' as registered with dndupload_handler (or 'Files') + * object $course the course the upload was for + * int $draftitemid optional the id of the draft area containing the files + * int $coursemodule id of the course module that has already been created + * string $displayname the name to use for this activity (can be overriden by the mod) + */ + protected function prepare_module_data($draftitemid = null, $content = null) { + $data = new stdClass(); + $data->type = $this->type; + $data->course = $this->course; + if ($draftitemid) { + $data->draftitemid = $draftitemid; + } else if ($content) { + $data->content = $content; + } + $data->coursemodule = $this->cm->id; + $data->displayname = $this->displayname; + return $data; + } + + /** + * Called after the mod has set itself up, to finish off any course module settings + * (set instance id, add to correct section, set visibility, etc.) and send the response + * + * @param int $instanceid id returned by the mod when it was created + */ + protected function finish_setup_course_module($instanceid) { + global $DB, $USER; + + if (!$instanceid) { + // Something has gone wrong - undo everything we can. + course_delete_module($this->cm->id); + throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); + } + + // Note the section visibility. + $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible; + + $DB->set_field('course_modules', 'instance', $instanceid, ['id' => $this->cm->id]); + + course_modinfo::purge_course_module_cache($this->course->id, $this->cm->id); + // Rebuild the course cache after update action. + rebuild_course_cache($this->course->id, true, true); + + $sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section, modname: $this->module->name); + + set_coursemodule_visible($this->cm->id, $visible); + if (!$visible) { + $DB->set_field('course_modules', 'visibleold', 1, ['id' => $this->cm->id]); + } + + // Retrieve the final info about this module. + $info = get_fast_modinfo($this->course); + if (!isset($info->cms[$this->cm->id])) { + // The course module has not been properly created in the course - undo everything. + course_delete_module($this->cm->id); + throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); + } + $mod = $info->get_cm($this->cm->id); + + // Trigger course module created event. + $event = \core\event\course_module_created::create_from_cm($mod); + $event->trigger(); + + $this->send_response($mod); + } + + /** + * Send the details of the newly created activity back to the client browser + * + * @param cm_info $mod details of the mod just created + */ + protected function send_response($mod) { + global $OUTPUT, $PAGE; + + $resp = new stdClass(); + $resp->error = self::ERROR_OK; + $resp->elementid = 'module-' . $mod->id; + $resp->cmid = $mod->id; + + $format = course_get_format($this->course); + $renderer = $format->get_renderer($PAGE); + $modinfo = $format->get_modinfo(); + $section = $modinfo->get_section_info($mod->sectionnum); + + // Get the new element html content. + $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $mod); + + echo $OUTPUT->header(); + echo json_encode($resp); + die(); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(dndupload_ajax_processor::class, \dndupload_ajax_processor::class); diff --git a/public/course/classes/dndupload_handler.php b/public/course/classes/dndupload_handler.php new file mode 100644 index 00000000000..0135df1e63d --- /dev/null +++ b/public/course/classes/dndupload_handler.php @@ -0,0 +1,342 @@ +. + +namespace core_course; + +use core\component; +use core\context\course as context_course; +use core\exception\coding_exception; +use repository; +use stdClass; + +/** + * Stores all the information about the available dndupload handlers + * + * @package core + * @copyright 2012 Davo Smith + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dndupload_handler { + /** + * @var array A list of all registered mime types that can be dropped onto a course + * along with the modules that will handle them. + */ + protected $types = []; + + /** + * @var array A list of the different file types (extensions) that different modules + * will handle. + */ + protected $filehandlers = []; + + /** + * @var context_course|null + */ + protected $context = null; + + /** + * Gather a list of dndupload handlers from the different mods + * + * @param object $course The course this is being added to (to check course_allowed_module() ) + * @param array|null $modnames An array of module names that are allowed in this course. + */ + public function __construct($course, $modnames = null) { + global $CFG, $PAGE; + + // Add some default types to handle. + // Note: 'Files' type is hard-coded into the Javascript as this needs to be ... + // ... treated a little differently. + $this->register_type( + identifier: 'url', + datatransfertypes: ['url', 'text/uri-list', 'text/x-moz-url'], + addmessage: get_string('addlinkhere', 'moodle'), + namemessage: get_string('nameforlink', 'moodle'), + handlermessage: get_string('whatforlink', 'moodle'), + priority: 10, + ); + $this->register_type( + identifier: 'text/html', + datatransfertypes: ['text/html'], + addmessage: get_string('addpagehere', 'moodle'), + namemessage: get_string('nameforpage', 'moodle'), + handlermessage: get_string('whatforpage', 'moodle'), + priority: 20, + ); + $this->register_type( + identifier: 'text', + datatransfertypes: ['text', 'text/plain'], + addmessage: get_string('addpagehere', 'moodle'), + namemessage: get_string('nameforpage', 'moodle'), + handlermessage: get_string('whatforpage', 'moodle'), + priority: 30, + ); + + $this->context = context_course::instance($course->id); + + // Loop through all modules to find handlers. + $mods = get_plugin_list_with_function('mod', 'dndupload_register'); + foreach ($mods as $component => $funcname) { + [$modtype, $modname] = component::normalize_component($component); + if ($modnames && !array_key_exists($modname, $modnames)) { + continue; // Module is deactivated (hidden) at the site level. + } + if (!course_allowed_module($course, $modname)) { + continue; // User does not have permission to add this module to the course. + } + $resp = $funcname(); + if (!$resp) { + continue; + } + if (isset($resp['files'])) { + foreach ($resp['files'] as $file) { + $this->register_file_handler($file['extension'], $modname, $file['message']); + } + } + if (isset($resp['addtypes'])) { + foreach ($resp['addtypes'] as $type) { + if (isset($type['priority'])) { + $priority = $type['priority']; + } else { + $priority = 100; + } + if (!isset($type['handlermessage'])) { + $type['handlermessage'] = ''; + } + $this->register_type( + identifier: $type['identifier'], + datatransfertypes: $type['datatransfertypes'], + addmessage: $type['addmessage'], + namemessage: $type['namemessage'], + handlermessage: $type['handlermessage'], + priority: $priority, + ); + } + } + if (isset($resp['types'])) { + foreach ($resp['types'] as $type) { + $noname = !empty($type['noname']); + $this->register_type_handler($type['identifier'], $modname, $type['message'], $noname); + } + } + $PAGE->requires->string_for_js('pluginname', $modname); + } + } + + /** + * Used to add a new mime type that can be drag and dropped onto a + * course displayed in a browser window + * + * @param string $identifier The name that this type will be known as + * @param array $datatransfertypes An array of the different types in the browser + * 'dataTransfer.types' object that will map to this type + * @param string $addmessage The message to display in the browser when this type is being + * dragged onto the page + * @param string $namemessage The message to pop up when asking for the name to give the + * course module instance when it is created + * @param string $handlermessage The message to pop up when asking which module should handle this type + * @param int $priority Controls the order in which types are checked by the browser (mainly + * needed to check for 'text' last as that is usually given as fallback) + */ + protected function register_type($identifier, $datatransfertypes, $addmessage, $namemessage, $handlermessage, $priority = 100) { + if ($this->is_known_type($identifier)) { + throw new coding_exception("Type $identifier is already registered"); + } + + $add = (object) [ + 'identifier' => $identifier, + 'datatransfertypes' => $datatransfertypes, + 'addmessage' => $addmessage, + 'namemessage' => $namemessage, + 'handlermessage' => $handlermessage, + 'priority' => $priority, + 'handlers' => [], + ]; + + $this->types[$identifier] = $add; + } + + /** + * Used to declare that a particular module will handle a particular type + * of dropped data + * + * @param string $type The name of the type (as declared in register_type) + * @param string $module The name of the module to handle this type + * @param string $message The message to show the user if more than one handler is registered + * for a type and the user needs to make a choice between them + * @param bool $noname If true, the 'name' dialog should be disabled in the pop-up. + * @throws coding_exception + */ + protected function register_type_handler($type, $module, $message, $noname) { + if (!$this->is_known_type($type)) { + throw new coding_exception("Trying to add handler for unknown type $type"); + } + + $add = (object) [ + 'type' => $type, + 'module' => $module, + 'message' => $message, + 'noname' => $noname ? 1 : 0, + ]; + + $this->types[$type]->handlers[] = $add; + } + + /** + * Used to declare that a particular module will handle a particular type + * of dropped file + * + * @param string $extension The file extension to handle ('*' for all types) + * @param string $module The name of the module to handle this type + * @param string $message The message to show the user if more than one handler is registered + * for a type and the user needs to make a choice between them + */ + protected function register_file_handler($extension, $module, $message) { + $extension = strtolower($extension); + + $add = (object) [ + 'extension' => $extension, + 'module' => $module, + 'message' => $message, + ]; + + $this->filehandlers[] = $add; + } + + /** + * Check to see if the type has been registered + * + * @param string $type The identifier of the type you are interested in + * @return bool True if the type is registered + */ + public function is_known_type($type) { + return array_key_exists($type, $this->types); + } + + /** + * Check to see if the module in question has registered to handle the + * type given + * + * @param string $module The name of the module + * @param string $type The identifier of the type + * @return bool True if the module has registered to handle that type + */ + public function has_type_handler($module, $type) { + if (!$this->is_known_type($type)) { + throw new coding_exception("Checking for handler for unknown type $type"); + } + foreach ($this->types[$type]->handlers as $handler) { + if ($handler->module == $module) { + return true; + } + } + return false; + } + + /** + * Check to see if the module in question has registered to handle files + * with the given extension (or to handle all file types) + * + * @param string $module The name of the module + * @param string $extension The extension of the uploaded file + * @return bool True if the module has registered to handle files with + * that extension (or to handle all file types) + */ + public function has_file_handler($module, $extension) { + foreach ($this->filehandlers as $handler) { + if ($handler->module == $module) { + if ($handler->extension == '*' || $handler->extension == $extension) { + return true; + } + } + } + return false; + } + + /** + * Gets a list of the file types that are handled by a particular module + * + * @param string $module The name of the module to check + * @return array of file extensions or string '*' + */ + public function get_handled_file_types($module) { + $types = []; + foreach ($this->filehandlers as $handler) { + if ($handler->module == $module) { + if ($handler->extension == '*') { + return '*'; + } else { + // Prepending '.' as otherwise mimeinfo fails. + $types[] = '.' . $handler->extension; + } + } + } + return $types; + } + + /** + * Returns an object to pass onto the javascript code with data about all the + * registered file / type handlers + * + * @return object Data to pass on to Javascript code + */ + public function get_js_data() { + global $CFG; + + $ret = new stdClass(); + + // Sort the types by priority. + uasort($this->types, [$this, 'type_compare']); + + $ret->types = []; + if (!empty($CFG->dndallowtextandlinks)) { + foreach ($this->types as $type) { + if (empty($type->handlers)) { + continue; // Skip any types without registered handlers. + } + $ret->types[] = $type; + } + } + + $ret->filehandlers = $this->filehandlers; + $uploadrepo = repository::get_instances(['type' => 'upload', 'currentcontext' => $this->context]); + if (empty($uploadrepo)) { + $ret->filehandlers = []; // No upload repo => no file handlers. + } + + return $ret; + } + + /** + * Comparison function used when sorting types by priority + * @param object $type1 first type to compare + * @param object $type2 second type to compare + * @return integer -1 for $type1 < $type2; 1 for $type1 > $type2; 0 for equal + */ + protected function type_compare($type1, $type2) { + if ($type1->priority < $type2->priority) { + return -1; + } + if ($type1->priority > $type2->priority) { + return 1; + } + return 0; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(dndupload_handler::class, \dndupload_handler::class); diff --git a/public/course/classes/form/reject_course_request.php b/public/course/classes/form/reject_course_request.php new file mode 100644 index 00000000000..6490784783a --- /dev/null +++ b/public/course/classes/form/reject_course_request.php @@ -0,0 +1,53 @@ +. + +namespace core_course\form; + +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/formslib.php"); + +/** + * A form for an administrator to reject a course request. + * + * @package core_course + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reject_course_request extends moodleform { + #[\Override] + public function definition() { + $mform =& $this->_form; + + $mform->addElement('hidden', 'reject', 0); + $mform->setType('reject', PARAM_INT); + + $mform->addElement('header', 'coursedetails', get_string('coursereasonforrejecting')); + + $mform->addElement( + 'textarea', + 'rejectnotice', + get_string('coursereasonforrejectingemail'), + ['rows' => '15', 'cols' => '50'], + ); + $mform->addRule('rejectnotice', get_string('missingreqreason'), 'required', null, 'client'); + $mform->setType('rejectnotice', PARAM_TEXT); + + $this->add_action_buttons(true, get_string('reject')); + } +} diff --git a/public/course/classes/form/request_course.php b/public/course/classes/form/request_course.php new file mode 100644 index 00000000000..a4e2630c494 --- /dev/null +++ b/public/course/classes/form/request_course.php @@ -0,0 +1,144 @@ +. + +namespace core_course\form; + +use core_course\course_request; +use core_course_category; +use moodleform; + +defined('MOODLE_INTERNAL') || die(); + +require_once("{$CFG->libdir}/formslib.php"); + +/** + * A form for a user to request a course. + * + * @package core_course + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class request_course extends moodleform { + #[\Override] + public function definition() { + global $CFG, $DB, $USER; + + $mform =& $this->_form; + + if ($pending = $DB->get_records('course_request', ['requester' => $USER->id])) { + $mform->addElement('header', 'pendinglist', get_string('coursespending')); + $list = []; + foreach ($pending as $cp) { + $list[] = format_string($cp->fullname); + } + $list = implode(', ', $list); + $mform->addElement('static', 'pendingcourses', get_string('courses'), $list); + } + + $mform->addElement('header', 'coursedetails', get_string('courserequestdetails')); + + $mform->addElement( + 'text', + 'fullname', + get_string('fullnamecourse'), + ['maxlength' => \core_course\constants::FULLNAME_MAXIMUM_LENGTH, 'size' => 50], + ); + $mform->addHelpButton('fullname', 'fullnamecourse'); + $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); + $mform->setType('fullname', PARAM_TEXT); + + $mform->addElement( + 'text', + 'shortname', + get_string('shortnamecourse'), + ['maxlength' => \core_course\constants::SHORTNAME_MAXIMUM_LENGTH, 'size' => 20], + ); + $mform->addHelpButton('shortname', 'shortnamecourse'); + $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); + $mform->setType('shortname', PARAM_TEXT); + + if (empty($CFG->lockrequestcategory)) { + $displaylist = core_course_category::make_categories_list('moodle/course:request'); + $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist); + $mform->addRule('category', null, 'required', null, 'client'); + $mform->setDefault('category', $CFG->defaultrequestcategory); + $mform->addHelpButton('category', 'coursecategory'); + } + + $mform->addElement( + 'editor', + 'summary_editor', + get_string('summary'), + null, + course_request::summary_editor_options(), + ); + $mform->addHelpButton('summary_editor', 'coursesummary'); + $mform->setType('summary_editor', PARAM_RAW); + + $mform->addElement('header', 'requestreason', get_string('courserequestreason')); + + $mform->addElement( + 'textarea', + 'reason', + get_string('courserequestsupport'), + ['rows' => '15', 'cols' => '50'], + ); + $mform->addRule('reason', get_string('missingreqreason'), 'required', null, 'client'); + $mform->setType('reason', PARAM_TEXT); + + $this->add_action_buttons(true, get_string('requestcourse')); + } + + #[\Override] + public function validation($data, $files) { + global $DB; + + $errors = parent::validation($data, $files); + $foundcourses = null; + $foundreqcourses = null; + + if (!empty($data['shortname'])) { + $foundcourses = $DB->get_records('course', ['shortname' => $data['shortname']]); + $foundreqcourses = $DB->get_records('course_request', ['shortname' => $data['shortname']]); + } + if (!empty($foundreqcourses)) { + if (!empty($foundcourses)) { + $foundcourses = array_merge($foundcourses, $foundreqcourses); + } else { + $foundcourses = $foundreqcourses; + } + } + + if (!empty($foundcourses)) { + foreach ($foundcourses as $foundcourse) { + if (!empty($foundcourse->requester)) { + $pending = 1; + $foundcoursenames[] = $foundcourse->fullname . ' [*]'; + } else { + $foundcoursenames[] = $foundcourse->fullname; + } + } + $foundcoursenamestring = implode(',', $foundcoursenames); + + $errors['shortname'] = get_string('shortnametaken', '', $foundcoursenamestring); + if (!empty($pending)) { + $errors['shortname'] .= get_string('starpending'); + } + } + + return $errors; + } +} diff --git a/public/course/classes/output/category_action_bar.php b/public/course/classes/output/category_action_bar.php index 0b6e4790536..a01097db4e4 100644 --- a/public/course/classes/output/category_action_bar.php +++ b/public/course/classes/output/category_action_bar.php @@ -20,7 +20,7 @@ use action_menu; use action_menu_link_secondary; use context_coursecat; use core_course_category; -use course_request; +use core_course\course_request; use moodle_page; use moodle_url; diff --git a/public/course/dndupload.php b/public/course/dndupload.php index 6653336773d..ac0836435ce 100644 --- a/public/course/dndupload.php +++ b/public/course/dndupload.php @@ -26,7 +26,6 @@ define('AJAX_SCRIPT', true); require_once(__DIR__ . '/../config.php'); -require_once($CFG->dirroot.'/course/dnduploadlib.php'); $courseid = required_param('course', PARAM_INT); $section = required_param('section', PARAM_INT); @@ -37,5 +36,5 @@ $contents = optional_param('contents', null, PARAM_RAW); // It will be up to eac $PAGE->set_url('/course/dndupload.php'); -$dndproc = new dndupload_ajax_processor($courseid, $section, $type, $modulename); +$dndproc = new \core_course\dndupload_ajax_processor($courseid, $section, $type, $modulename); $dndproc->process($displayname, $contents); diff --git a/public/course/dnduploadlib.php b/public/course/dnduploadlib.php index e2383d3d53b..41f83b53eb7 100644 --- a/public/course/dnduploadlib.php +++ b/public/course/dnduploadlib.php @@ -25,8 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot.'/repository/lib.php'); -require_once($CFG->dirroot.'/repository/upload/lib.php'); require_once($CFG->dirroot.'/course/lib.php'); /** @@ -90,593 +88,3 @@ function dndupload_add_to_course($course, $modnames) { $PAGE->requires->js_init_call('M.course_dndupload.init', $vars, true, $jsmodule); } - - -/** - * Stores all the information about the available dndupload handlers - * - * @package core - * @copyright 2012 Davo Smith - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class dndupload_handler { - - /** - * @var array A list of all registered mime types that can be dropped onto a course - * along with the modules that will handle them. - */ - protected $types = array(); - - /** - * @var array A list of the different file types (extensions) that different modules - * will handle. - */ - protected $filehandlers = array(); - - /** - * @var context_course|null - */ - protected $context = null; - - /** - * Gather a list of dndupload handlers from the different mods - * - * @param object $course The course this is being added to (to check course_allowed_module() ) - */ - public function __construct($course, $modnames = null) { - global $CFG, $PAGE; - - // Add some default types to handle. - // Note: 'Files' type is hard-coded into the Javascript as this needs to be ... - // ... treated a little differently. - $this->register_type('url', array('url', 'text/uri-list', 'text/x-moz-url'), get_string('addlinkhere', 'moodle'), - get_string('nameforlink', 'moodle'), get_string('whatforlink', 'moodle'), 10); - $this->register_type('text/html', array('text/html'), get_string('addpagehere', 'moodle'), - get_string('nameforpage', 'moodle'), get_string('whatforpage', 'moodle'), 20); - $this->register_type('text', array('text', 'text/plain'), get_string('addpagehere', 'moodle'), - get_string('nameforpage', 'moodle'), get_string('whatforpage', 'moodle'), 30); - - $this->context = context_course::instance($course->id); - - // Loop through all modules to find handlers. - $mods = get_plugin_list_with_function('mod', 'dndupload_register'); - foreach ($mods as $component => $funcname) { - list($modtype, $modname) = core_component::normalize_component($component); - if ($modnames && !array_key_exists($modname, $modnames)) { - continue; // Module is deactivated (hidden) at the site level. - } - if (!course_allowed_module($course, $modname)) { - continue; // User does not have permission to add this module to the course. - } - $resp = $funcname(); - if (!$resp) { - continue; - } - if (isset($resp['files'])) { - foreach ($resp['files'] as $file) { - $this->register_file_handler($file['extension'], $modname, $file['message']); - } - } - if (isset($resp['addtypes'])) { - foreach ($resp['addtypes'] as $type) { - if (isset($type['priority'])) { - $priority = $type['priority']; - } else { - $priority = 100; - } - if (!isset($type['handlermessage'])) { - $type['handlermessage'] = ''; - } - $this->register_type($type['identifier'], $type['datatransfertypes'], - $type['addmessage'], $type['namemessage'], $type['handlermessage'], $priority); - } - } - if (isset($resp['types'])) { - foreach ($resp['types'] as $type) { - $noname = !empty($type['noname']); - $this->register_type_handler($type['identifier'], $modname, $type['message'], $noname); - } - } - $PAGE->requires->string_for_js('pluginname', $modname); - } - } - - /** - * Used to add a new mime type that can be drag and dropped onto a - * course displayed in a browser window - * - * @param string $identifier The name that this type will be known as - * @param array $datatransfertypes An array of the different types in the browser - * 'dataTransfer.types' object that will map to this type - * @param string $addmessage The message to display in the browser when this type is being - * dragged onto the page - * @param string $namemessage The message to pop up when asking for the name to give the - * course module instance when it is created - * @param string $handlermessage The message to pop up when asking which module should handle this type - * @param int $priority Controls the order in which types are checked by the browser (mainly - * needed to check for 'text' last as that is usually given as fallback) - */ - protected function register_type($identifier, $datatransfertypes, $addmessage, $namemessage, $handlermessage, $priority=100) { - if ($this->is_known_type($identifier)) { - throw new coding_exception("Type $identifier is already registered"); - } - - $add = new stdClass; - $add->identifier = $identifier; - $add->datatransfertypes = $datatransfertypes; - $add->addmessage = $addmessage; - $add->namemessage = $namemessage; - $add->handlermessage = $handlermessage; - $add->priority = $priority; - $add->handlers = array(); - - $this->types[$identifier] = $add; - } - - /** - * Used to declare that a particular module will handle a particular type - * of dropped data - * - * @param string $type The name of the type (as declared in register_type) - * @param string $module The name of the module to handle this type - * @param string $message The message to show the user if more than one handler is registered - * for a type and the user needs to make a choice between them - * @param bool $noname If true, the 'name' dialog should be disabled in the pop-up. - * @throws coding_exception - */ - protected function register_type_handler($type, $module, $message, $noname) { - if (!$this->is_known_type($type)) { - throw new coding_exception("Trying to add handler for unknown type $type"); - } - - $add = new stdClass; - $add->type = $type; - $add->module = $module; - $add->message = $message; - $add->noname = $noname ? 1 : 0; - - $this->types[$type]->handlers[] = $add; - } - - /** - * Used to declare that a particular module will handle a particular type - * of dropped file - * - * @param string $extension The file extension to handle ('*' for all types) - * @param string $module The name of the module to handle this type - * @param string $message The message to show the user if more than one handler is registered - * for a type and the user needs to make a choice between them - */ - protected function register_file_handler($extension, $module, $message) { - $extension = strtolower($extension); - - $add = new stdClass; - $add->extension = $extension; - $add->module = $module; - $add->message = $message; - - $this->filehandlers[] = $add; - } - - /** - * Check to see if the type has been registered - * - * @param string $type The identifier of the type you are interested in - * @return bool True if the type is registered - */ - public function is_known_type($type) { - return array_key_exists($type, $this->types); - } - - /** - * Check to see if the module in question has registered to handle the - * type given - * - * @param string $module The name of the module - * @param string $type The identifier of the type - * @return bool True if the module has registered to handle that type - */ - public function has_type_handler($module, $type) { - if (!$this->is_known_type($type)) { - throw new coding_exception("Checking for handler for unknown type $type"); - } - foreach ($this->types[$type]->handlers as $handler) { - if ($handler->module == $module) { - return true; - } - } - return false; - } - - /** - * Check to see if the module in question has registered to handle files - * with the given extension (or to handle all file types) - * - * @param string $module The name of the module - * @param string $extension The extension of the uploaded file - * @return bool True if the module has registered to handle files with - * that extension (or to handle all file types) - */ - public function has_file_handler($module, $extension) { - foreach ($this->filehandlers as $handler) { - if ($handler->module == $module) { - if ($handler->extension == '*' || $handler->extension == $extension) { - return true; - } - } - } - return false; - } - - /** - * Gets a list of the file types that are handled by a particular module - * - * @param string $module The name of the module to check - * @return array of file extensions or string '*' - */ - public function get_handled_file_types($module) { - $types = array(); - foreach ($this->filehandlers as $handler) { - if ($handler->module == $module) { - if ($handler->extension == '*') { - return '*'; - } else { - // Prepending '.' as otherwise mimeinfo fails. - $types[] = '.'.$handler->extension; - } - } - } - return $types; - } - - /** - * Returns an object to pass onto the javascript code with data about all the - * registered file / type handlers - * - * @return object Data to pass on to Javascript code - */ - public function get_js_data() { - global $CFG; - - $ret = new stdClass; - - // Sort the types by priority. - uasort($this->types, array($this, 'type_compare')); - - $ret->types = array(); - if (!empty($CFG->dndallowtextandlinks)) { - foreach ($this->types as $type) { - if (empty($type->handlers)) { - continue; // Skip any types without registered handlers. - } - $ret->types[] = $type; - } - } - - $ret->filehandlers = $this->filehandlers; - $uploadrepo = repository::get_instances(array('type' => 'upload', 'currentcontext' => $this->context)); - if (empty($uploadrepo)) { - $ret->filehandlers = array(); // No upload repo => no file handlers. - } - - return $ret; - } - - /** - * Comparison function used when sorting types by priority - * @param object $type1 first type to compare - * @param object $type2 second type to compare - * @return integer -1 for $type1 < $type2; 1 for $type1 > $type2; 0 for equal - */ - protected function type_compare($type1, $type2) { - if ($type1->priority < $type2->priority) { - return -1; - } - if ($type1->priority > $type2->priority) { - return 1; - } - return 0; - } - -} - -/** - * Processes the upload, creating the course module and returning the result - * - * @package core - * @copyright 2012 Davo Smith - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class dndupload_ajax_processor { - - /** Returned when no error has occurred */ - const ERROR_OK = 0; - - /** @var object The course that we are uploading to */ - protected $course = null; - - /** @var context_course The course context for capability checking */ - protected $context = null; - - /** @var int The section number we are uploading to */ - protected $section = null; - - /** @var string The type of upload (e.g. 'Files', 'text/plain') */ - protected $type = null; - - /** @var object The details of the module type that will be created */ - protected $module= null; - - /** @var object The course module that has been created */ - protected $cm = null; - - /** @var dndupload_handler used to check the allowed file types */ - protected $dnduploadhandler = null; - - /** @var string The name to give the new activity instance */ - protected $displayname = null; - - /** - * Set up some basic information needed to handle the upload - * - * @param int $courseid The ID of the course we are uploading to - * @param int $section The section number we are uploading to - * @param string $type The type of upload (as reported by the browser) - * @param string $modulename The name of the module requested to handle this upload - */ - public function __construct($courseid, $section, $type, $modulename) { - global $DB; - - if (!defined('AJAX_SCRIPT')) { - throw new coding_exception('dndupload_ajax_processor should only be used within AJAX requests'); - } - - $this->course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); - - require_login($this->course, false); - $this->context = context_course::instance($this->course->id); - - if (!is_number($section) || $section < 0) { - throw new coding_exception("Invalid section number $section"); - } - $this->section = $section; - $this->type = $type; - - if (!$this->module = $DB->get_record('modules', array('name' => $modulename))) { - throw new coding_exception("Module $modulename does not exist"); - } - - $this->dnduploadhandler = new dndupload_handler($this->course); - } - - /** - * Check if this upload is a 'file' upload - * - * @return bool true if it is a 'file' upload, false otherwise - */ - protected function is_file_upload() { - return ($this->type == 'Files'); - } - - /** - * Process the upload - creating the module in the course and returning the result to the browser - * - * @param string $displayname optional the name (from the browser) to give the course module instance - * @param string $content optional the content of the upload (for non-file uploads) - */ - public function process($displayname = null, $content = null) { - require_capability('moodle/course:manageactivities', $this->context); - - if ($this->is_file_upload()) { - require_capability('moodle/course:managefiles', $this->context); - if ($content != null) { - throw new moodle_exception('fileuploadwithcontent', 'moodle'); - } - } else { - if (empty($content)) { - throw new moodle_exception('dnduploadwithoutcontent', 'moodle'); - } - } - - require_sesskey(); - - $this->displayname = $displayname; - - if ($this->is_file_upload()) { - $this->handle_file_upload(); - } else { - $this->handle_other_upload($content); - } - } - - /** - * Handle uploads containing files - create the course module, ask the upload repository - * to process the file, ask the mod to set itself up, then return the result to the browser - */ - protected function handle_file_upload() { - global $CFG; - - // Add the file to a draft file area. - $draftitemid = file_get_unused_draft_itemid(); - $maxbytes = get_user_max_upload_file_size($this->context, $CFG->maxbytes, $this->course->maxbytes); - $types = $this->dnduploadhandler->get_handled_file_types($this->module->name); - $repo = repository::get_instances(array('type' => 'upload', 'currentcontext' => $this->context)); - if (empty($repo)) { - throw new moodle_exception('errornouploadrepo', 'moodle'); - } - $repo = reset($repo); // Get the first (and only) upload repo. - // Pre-emptively purge the navigation cache so the upload repo can close the session. - navigation_cache::destroy_volatile_caches(); - $details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid); - if (empty($this->displayname)) { - $this->displayname = $this->display_name_from_file($details['file']); - } - - // Create a course module to hold the new instance. - $this->create_course_module(); - - // Ask the module to set itself up. - $moduledata = $this->prepare_module_data($draftitemid); - $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction'); - if ($instanceid === 'invalidfunction') { - throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function"); - } - - // Finish setting up the course module. - $this->finish_setup_course_module($instanceid); - } - - /** - * Handle uploads not containing file - create the course module, ask the mod to - * set itself up, then return the result to the browser - * - * @param string $content the content uploaded to the browser - */ - protected function handle_other_upload($content) { - // Check this plugin is registered to handle this type of upload - if (!$this->dnduploadhandler->has_type_handler($this->module->name, $this->type)) { - $info = (object)array('modname' => $this->module->name, 'type' => $this->type); - throw new moodle_exception('moddoesnotsupporttype', 'moodle', $info); - } - - // Create a course module to hold the new instance. - $this->create_course_module(); - - // Ask the module to set itself up. - $moduledata = $this->prepare_module_data(null, $content); - $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction'); - if ($instanceid === 'invalidfunction') { - throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function"); - } - - // Finish setting up the course module. - $this->finish_setup_course_module($instanceid); - } - - /** - * Generate the name of the mod instance from the name of the file - * (remove the extension and convert underscore => space - * - * @param string $filename the filename of the uploaded file - * @return string the display name to use - */ - protected function display_name_from_file($filename) { - $pos = core_text::strrpos($filename, '.'); - if ($pos) { // Want to skip if $pos === 0 OR $pos === false. - $filename = core_text::substr($filename, 0, $pos); - } - return str_replace('_', ' ', $filename); - } - - /** - * Create the coursemodule to hold the file/content that has been uploaded - */ - protected function create_course_module() { - global $CFG; - require_once($CFG->dirroot.'/course/modlib.php'); - list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($this->course, $this->module->name, $this->section); - - $data->coursemodule = $data->id = add_course_module($data); - $this->cm = $data; - } - - /** - * Gather together all the details to pass on to the mod, so that it can initialise it's - * own database tables - * - * @param int $draftitemid optional the id of the draft area containing the file (for file uploads) - * @param string $content optional the content dropped onto the course (for non-file uploads) - * @return object data to pass on to the mod, containing: - * string $type the 'type' as registered with dndupload_handler (or 'Files') - * object $course the course the upload was for - * int $draftitemid optional the id of the draft area containing the files - * int $coursemodule id of the course module that has already been created - * string $displayname the name to use for this activity (can be overriden by the mod) - */ - protected function prepare_module_data($draftitemid = null, $content = null) { - $data = new stdClass(); - $data->type = $this->type; - $data->course = $this->course; - if ($draftitemid) { - $data->draftitemid = $draftitemid; - } else if ($content) { - $data->content = $content; - } - $data->coursemodule = $this->cm->id; - $data->displayname = $this->displayname; - return $data; - } - - /** - * Called after the mod has set itself up, to finish off any course module settings - * (set instance id, add to correct section, set visibility, etc.) and send the response - * - * @param int $instanceid id returned by the mod when it was created - */ - protected function finish_setup_course_module($instanceid) { - global $DB, $USER; - - if (!$instanceid) { - // Something has gone wrong - undo everything we can. - course_delete_module($this->cm->id); - throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); - } - - // Note the section visibility - $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible; - - $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id)); - - \course_modinfo::purge_course_module_cache($this->course->id, $this->cm->id); - // Rebuild the course cache after update action - rebuild_course_cache($this->course->id, true, true); - - $sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section, modname: $this->module->name); - - set_coursemodule_visible($this->cm->id, $visible); - if (!$visible) { - $DB->set_field('course_modules', 'visibleold', 1, array('id' => $this->cm->id)); - } - - // retrieve the final info about this module. - $info = get_fast_modinfo($this->course); - if (!isset($info->cms[$this->cm->id])) { - // The course module has not been properly created in the course - undo everything. - course_delete_module($this->cm->id); - throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); - } - $mod = $info->get_cm($this->cm->id); - - // Trigger course module created event. - $event = \core\event\course_module_created::create_from_cm($mod); - $event->trigger(); - - $this->send_response($mod); - } - - /** - * Send the details of the newly created activity back to the client browser - * - * @param cm_info $mod details of the mod just created - */ - protected function send_response($mod) { - global $OUTPUT, $PAGE; - - $resp = new stdClass(); - $resp->error = self::ERROR_OK; - $resp->elementid = 'module-' . $mod->id; - $resp->cmid = $mod->id; - - $format = course_get_format($this->course); - $renderer = $format->get_renderer($PAGE); - $modinfo = $format->get_modinfo(); - $section = $modinfo->get_section_info($mod->sectionnum); - - // Get the new element html content. - $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $mod); - - echo $OUTPUT->header(); - echo json_encode($resp); - die(); - } -} diff --git a/public/course/format/classes/external/file_handlers.php b/public/course/format/classes/external/file_handlers.php index 2d8721ff271..3b89300b9a2 100644 --- a/public/course/format/classes/external/file_handlers.php +++ b/public/course/format/classes/external/file_handlers.php @@ -16,17 +16,12 @@ namespace core_courseformat\external; -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/course/dnduploadlib.php'); - +use core_course\dndupload_handler; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; -use dndupload_handler; /** * Class for exporting a course file handlers. diff --git a/public/course/format/tests/external/file_handlers_test.php b/public/course/format/tests/external/file_handlers_test.php index b9bb071b9f9..a2dcec7ff4e 100644 --- a/public/course/format/tests/external/file_handlers_test.php +++ b/public/course/format/tests/external/file_handlers_test.php @@ -16,15 +16,9 @@ namespace core_courseformat\external; +use core_course\dndupload_handler; use core_external\external_api; -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot . '/webservice/tests/helpers.php'); - -use dndupload_handler; - /** * Tests for the file_hanlders class. * @@ -42,7 +36,6 @@ final class file_handlers_test extends \externallib_advanced_testcase { public static function setupBeforeClass(): void { // phpcs:ignore global $CFG; require_once($CFG->dirroot . '/course/lib.php'); - require_once($CFG->dirroot . '/course/dnduploadlib.php'); } /** diff --git a/public/course/lib.php b/public/course/lib.php index 2ef5dde71fb..c74f29c54e3 100644 --- a/public/course/lib.php +++ b/public/course/lib.php @@ -26,7 +26,6 @@ defined('MOODLE_INTERNAL') || die; use core\di; use core\hook; -use core_course\external\course_summary_exporter; use core_courseformat\base as course_format; use core_courseformat\formatactions; use core_courseformat\sectiondelegate; @@ -2333,424 +2332,6 @@ function average_number_of_courses_modules() { return $coursemoduleaverage; } -/** - * This class pertains to course requests and contains methods associated with - * create, approving, and removing course requests. - * - * Please note we do not allow embedded images here because there is no context - * to store them with proper access control. - * - * @copyright 2009 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.0 - * - * @property-read int $id - * @property-read string $fullname - * @property-read string $shortname - * @property-read string $summary - * @property-read int $summaryformat - * @property-read int $summarytrust - * @property-read string $reason - * @property-read int $requester - */ -class course_request { - - /** - * This is the stdClass that stores the properties for the course request - * and is externally accessed through the __get magic method - * @var stdClass - */ - protected $properties; - - /** - * An array of options for the summary editor used by course request forms. - * This is initially set by {@link summary_editor_options()} - * @var array - * @static - */ - protected static $summaryeditoroptions; - - /** - * Static function to prepare the summary editor for working with a course - * request. - * - * @static - * @param null|stdClass $data Optional, an object containing the default values - * for the form, these may be modified when preparing the - * editor so this should be called before creating the form - * @return stdClass An object that can be used to set the default values for - * an mforms form - */ - public static function prepare($data=null) { - if ($data === null) { - $data = new stdClass; - } - $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options()); - return $data; - } - - /** - * Static function to create a new course request when passed an array of properties - * for it. - * - * This function also handles saving any files that may have been used in the editor - * - * @static - * @param stdClass $data - * @return course_request The newly created course request - */ - public static function create($data) { - global $USER, $DB, $CFG; - $data->requester = $USER->id; - - // Setting the default category if none set. - if (empty($data->category) || !empty($CFG->lockrequestcategory)) { - $data->category = $CFG->defaultrequestcategory; - } - - // Summary is a required field so copy the text over - $data->summary = $data->summary_editor['text']; - $data->summaryformat = $data->summary_editor['format']; - - $data->id = $DB->insert_record('course_request', $data); - - // Create a new course_request object and return it - $request = new course_request($data); - - // Notify the admin if required. - if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) { - - $a = new stdClass; - $a->link = "$CFG->wwwroot/course/pending.php"; - $a->user = fullname($USER); - $a->shortname = s($data->shortname) ?? ''; - $a->fullname = s($data->fullname) ?? ''; - $a->category = s($data->category) ?? ''; - $a->reason = format_text($data->reason, FORMAT_PLAIN) ?? ''; - $subject = get_string('courserequest'); - $message = get_string('courserequestnotifyemail', 'admin', $a); - foreach ($users as $user) { - $request->notify($user, $USER, 'courserequested', $subject, $message); - } - } - - return $request; - } - - /** - * Returns an array of options to use with a summary editor - * - * @uses course_request::$summaryeditoroptions - * @return array An array of options to use with the editor - */ - public static function summary_editor_options() { - global $CFG; - if (self::$summaryeditoroptions === null) { - self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0); - } - return self::$summaryeditoroptions; - } - - /** - * Loads the properties for this course request object. Id is required and if - * only id is provided then we load the rest of the properties from the database - * - * @param stdClass|int $properties Either an object containing properties - * or the course_request id to load - */ - public function __construct($properties) { - global $DB; - if (empty($properties->id)) { - if (empty($properties)) { - throw new coding_exception('You must provide a course request id when creating a course_request object'); - } - $id = $properties; - $properties = new stdClass; - $properties->id = (int)$id; - unset($id); - } - if (empty($properties->requester)) { - if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) { - throw new \moodle_exception('unknowncourserequest'); - } - } else { - $this->properties = $properties; - } - $this->properties->collision = null; - } - - /** - * Returns the requested property - * - * @param string $key - * @return mixed - */ - public function __get($key) { - return $this->properties->$key; - } - - /** - * Override this to ensure empty($request->blah) calls return a reliable answer... - * - * This is required because we define the __get method - * - * @param mixed $key - * @return bool True is it not empty, false otherwise - */ - public function __isset($key) { - return (!empty($this->properties->$key)); - } - - /** - * Returns the user who requested this course - * - * Uses a static var to cache the results and cut down the number of db queries - * - * @staticvar array $requesters An array of cached users - * @return stdClass The user who requested the course - */ - public function get_requester() { - global $DB; - static $requesters= array(); - if (!array_key_exists($this->properties->requester, $requesters)) { - $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester)); - } - return $requesters[$this->properties->requester]; - } - - /** - * Checks that the shortname used by the course does not conflict with any other - * courses that exist - * - * @param string|null $shortnamemark The string to append to the requests shortname - * should a conflict be found - * @return bool true is there is a conflict, false otherwise - */ - public function check_shortname_collision($shortnamemark = '[*]') { - global $DB; - - if ($this->properties->collision !== null) { - return $this->properties->collision; - } - - if (empty($this->properties->shortname)) { - debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER); - $this->properties->collision = false; - } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) { - if (!empty($shortnamemark)) { - $this->properties->shortname .= ' '.$shortnamemark; - } - $this->properties->collision = true; - } else { - $this->properties->collision = false; - } - return $this->properties->collision; - } - - /** - * Checks user capability to approve a requested course - * - * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is - * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'. - * - * @return bool - */ - public function can_approve() { - global $CFG; - $category = null; - if ($this->properties->category) { - $category = core_course_category::get($this->properties->category, IGNORE_MISSING); - } else if ($CFG->defaultrequestcategory) { - $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING); - } - if ($category) { - return has_capability('moodle/site:approvecourse', $category->get_context()); - } - - // We can not determine the context where the course should be created. The approver should have - // both capabilities to approve courses and change course category in the system context. - return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance()); - } - - /** - * Returns the category where this course request should be created - * - * Note that we don't check here that user has a capability to view - * hidden categories if he has capabilities 'moodle/site:approvecourse' and - * 'moodle/course:changecategory' - * - * @return core_course_category - */ - public function get_category() { - global $CFG; - if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) { - return $category; - } else if ($CFG->defaultrequestcategory && - ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) { - return $category; - } else { - return core_course_category::get_default(); - } - } - - /** - * This function approves the request turning it into a course - * - * This function converts the course request into a course, at the same time - * transferring any files used in the summary to the new course and then removing - * the course request and the files associated with it. - * - * @return int The id of the course that was created from this request - */ - public function approve() { - global $CFG, $DB, $USER; - - require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); - - $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST); - - $courseconfig = get_config('moodlecourse'); - - // Transfer appropriate settings - $data = clone($this->properties); - unset($data->id); - unset($data->reason); - unset($data->requester); - - // Set category - $category = $this->get_category(); - $data->category = $category->id; - // Set misc settings - $data->requested = 1; - - // Apply course default settings - $data->format = $courseconfig->format; - $data->newsitems = $courseconfig->newsitems; - $data->showgrades = $courseconfig->showgrades; - $data->showreports = $courseconfig->showreports; - $data->maxbytes = $courseconfig->maxbytes; - $data->groupmode = $courseconfig->groupmode; - $data->groupmodeforce = $courseconfig->groupmodeforce; - $data->visible = $courseconfig->visible; - $data->visibleold = $data->visible; - $data->lang = $courseconfig->lang; - $data->enablecompletion = $courseconfig->enablecompletion; - $data->numsections = $courseconfig->numsections; - $data->startdate = usergetmidnight(time()); - if ($courseconfig->courseenddateenabled) { - $data->enddate = usergetmidnight(time()) + $courseconfig->courseduration; - } - - list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname); - - $course = create_course($data); - $context = context_course::instance($course->id, MUST_EXIST); - - // add enrol instances - if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) { - if ($manual = enrol_get_plugin('manual')) { - $manual->add_default_instance($course); - } - } - - // enrol the requester as teacher if necessary - if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) { - enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid); - } - - $this->delete(); - - $a = new stdClass(); - $a->name = format_string($course->fullname, true, ['context' => $context]); - $a->url = course_get_url($course); - - $usernameplaceholders = \core\user::get_name_placeholders($user); - foreach ($usernameplaceholders as $field => $value) { - $a->{$field} = $value; - } - - $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id); - - return $course->id; - } - - /** - * Reject a course request - * - * This function rejects a course request, emailing the requesting user the - * provided notice and then removing the request from the database - * - * @param string $notice The message to display to the user - */ - public function reject($notice) { - global $USER, $DB; - $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST); - $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice)); - $this->delete(); - } - - /** - * Deletes the course request and any associated files - */ - public function delete() { - global $DB; - $DB->delete_records('course_request', array('id' => $this->properties->id)); - } - - /** - * Send a message from one user to another using events_trigger - * - * @param object $touser - * @param object $fromuser - * @param string $name - * @param string $subject - * @param string $message - * @param int|null $courseid - */ - protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) { - $eventdata = new \core\message\message(); - $eventdata->courseid = empty($courseid) ? SITEID : $courseid; - $eventdata->component = 'moodle'; - $eventdata->name = $name; - $eventdata->userfrom = $fromuser; - $eventdata->userto = $touser; - $eventdata->subject = $subject; - $eventdata->fullmessage = $message; - $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = ''; - $eventdata->smallmessage = ''; - $eventdata->notification = 1; - message_send($eventdata); - } - - /** - * Checks if current user can request a course in this context - * - * @param context $context - * @return bool - */ - public static function can_request(context $context) { - global $CFG; - if (empty($CFG->enablecourserequests)) { - return false; - } - if (has_capability('moodle/course:create', $context)) { - return false; - } - - if ($context instanceof context_system) { - $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING); - return $defaultcontext && - has_capability('moodle/course:request', $defaultcontext); - } else if ($context instanceof context_coursecat) { - if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) { - return has_capability('moodle/course:request', $context); - } - } - return false; - } -} - /** * Return a list of page types * @param string $pagetype current page type diff --git a/public/course/pending.php b/public/course/pending.php index f529fff27eb..f0812d39895 100644 --- a/public/course/pending.php +++ b/public/course/pending.php @@ -1,39 +1,37 @@ . /** * Allow the administrator to look through a list of course requests and approve or reject them. * - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package course + * @copyright Moodle Pty Ltd */ +use core\context\course as context_course; +use core\context\system as context_system; +use core\exception\moodle_exception; +use core\url; +use core\output\html_writer; +use core_course\form\reject_course_request as reject_course_request_form; + require_once(__DIR__ . '/../config.php'); require_once($CFG->libdir . '/adminlib.php'); require_once($CFG->dirroot . '/course/lib.php'); -require_once($CFG->dirroot . '/course/request_form.php'); $approve = optional_param('approve', 0, PARAM_INT); $reject = optional_param('reject', 0, PARAM_INT); @@ -51,58 +49,56 @@ if (has_capability('moodle/site:approvecourse', $context)) { require_capability('moodle/site:approvecourse', $context); } $PAGE->set_context($context); - $PAGE->set_url(new moodle_url('/course/pending.php')); + $PAGE->set_url(new url('/course/pending.php')); } -/// Process approval of a course. -if (!empty($approve) and confirm_sesskey()) { - /// Load the request. +// Process approval of a course. +if (!empty($approve) && confirm_sesskey()) { + // Load the request. $course = new course_request($approve); $courseid = $course->approve(); if ($courseid !== false) { if (has_capability('moodle/course:update', context_course::instance($courseid))) { - redirect(new moodle_url('/course/edit.php', ['id' => $courseid, 'returnto' => 'pending'])); + redirect(new url('/course/edit.php', ['id' => $courseid, 'returnto' => 'pending'])); } else { - redirect(new moodle_url('/course/view.php', ['id' => $courseid])); + redirect(new url('/course/view.php', ['id' => $courseid])); } } else { - throw new \moodle_exception('courseapprovedfailed'); + throw new moodle_exception('courseapprovedfailed'); } } -/// Process rejection of a course. +// Process rejection of a course. if (!empty($reject)) { // Load the request. $course = new course_request($reject); // Prepare the form. - $rejectform = new reject_request_form($baseurl); + $rejectform = new reject_course_request_form($baseurl); $default = new stdClass(); $default->reject = $course->id; $rejectform->set_data($default); -/// Standard form processing if statement. - if ($rejectform->is_cancelled()){ + // Standard form processing if statement. + if ($rejectform->is_cancelled()) { redirect($baseurl); - } else if ($data = $rejectform->get_data()) { - - /// Reject the request + // Reject the request. $course->reject($data->rejectnotice); - /// Redirect back to the course listing. + // Redirect back to the course listing. redirect($baseurl, get_string('courserejected')); } -/// Display the form for giving a reason for rejecting the request. + // Display the form for giving a reason for rejecting the request. echo $OUTPUT->header($rejectform->focus()); $rejectform->display(); echo $OUTPUT->footer(); exit; } -/// Print a list of all the pending requests. +// Print a list of all the pending requests. echo $OUTPUT->header(); $pending = $DB->get_records('course_request'); @@ -115,16 +111,16 @@ if (empty($pending)) { if ($role) { echo $OUTPUT->notification(get_string('courserequestwarning', 'core', role_get_name($role)), 'notifyproblem'); } else { - $userpoliciesurl = new moodle_url('/admin/settings.php', ['section' => 'userpolicies']); + $userpoliciesurl = new url('/admin/settings.php', ['section' => 'userpolicies']); echo $OUTPUT->notification(get_string('courserequestroleerror', 'core', (string) $userpoliciesurl), 'notifyerror'); } -/// Build a table of all the requests. + // Build a table of all the requests. $table = new html_table(); $table->attributes['class'] = 'pendingcourserequests table generaltable'; - $table->align = array('center', 'center', 'center', 'center', 'center', 'center'); - $table->head = array(get_string('requestedby'), get_string('shortnamecourse'), get_string('fullnamecourse'), - get_string('summary'), get_string('category'), get_string('requestreason'), get_string('action')); + $table->align = ['center', 'center', 'center', 'center', 'center', 'center']; + $table->head = [get_string('requestedby'), get_string('shortnamecourse'), get_string('fullnamecourse'), + get_string('summary'), get_string('category'), get_string('requestreason'), get_string('action')]; foreach ($pending as $course) { $course = new course_request($course); @@ -138,34 +134,51 @@ if (empty($pending)) { $category = $course->get_category(); // Fullname of the user who requested the course (with link to profile if current user can view it). - $requesterfullname = $OUTPUT->user_picture($course->get_requester(), [ - 'includefullname' => true, - 'link' => user_can_view_profile($course->get_requester()), - ]); + $requesterfullname = $OUTPUT->user_picture( + $course->get_requester(), + [ + 'includefullname' => true, + 'link' => user_can_view_profile($course->get_requester()), + ], + ); - $row = array(); - $row[] = $requesterfullname; - $row[] = format_string($course->shortname); - $row[] = format_string($course->fullname); - $row[] = format_text($course->summary, $course->summaryformat); - $row[] = $category->get_formatted_name(); - $row[] = format_string($course->reason); - $row[] = $OUTPUT->single_button(new moodle_url($baseurl, array('approve' => $course->id, 'sesskey' => sesskey())), get_string('approve'), 'get') . - $OUTPUT->single_button(new moodle_url($baseurl, array('reject' => $course->id)), get_string('rejectdots'), 'get'); + $row = [ + $requesterfullname, + format_string($course->shortname), + format_string($course->fullname), + format_text($course->summary, $course->summaryformat), + $category->get_formatted_name(), + format_string($course->reason), + ]; - /// Add the row to the table. + $controls = [ + $OUTPUT->single_button( + new url($baseurl, ['approve' => $course->id, 'sesskey' => sesskey()]), + get_string('approve'), + 'get', + ), + $OUTPUT->single_button( + new url($baseurl, ['reject' => $course->id]), + get_string('rejectdots'), + 'get', + ), + ]; + + $row[] = implode(' ', $controls); + + // Add the row to the table. $table->data[] = $row; } -/// Display the table. + // Display the table. echo html_writer::table($table); -/// Message about name collisions, if necessary. + // Message about name collisions, if necessary. if (!empty($collision)) { print_string('shortnamecollisionwarning'); } } -/// Finish off the page. +// Finish off the page. echo $OUTPUT->single_button($CFG->wwwroot . '/course/index.php', get_string('backtocourselisting')); echo $OUTPUT->footer(); diff --git a/public/course/request.php b/public/course/request.php index d27e860a5f8..3986401de04 100644 --- a/public/course/request.php +++ b/public/course/request.php @@ -1,5 +1,4 @@ . +use core\context\coursecat as context_coursecat; +use core\exception\moodle_exception; +use core\url; +use core_course\course_request; +use core_course\form\request_course as request_course_form; + /** * Allows a user to request a course be created for them. * @@ -25,17 +30,16 @@ require_once(__DIR__ . '/../config.php'); require_once($CFG->dirroot . '/course/lib.php'); -require_once($CFG->dirroot . '/course/request_form.php'); // Where we came from. Used in a number of redirects. -$url = new moodle_url('/course/request.php'); +$url = new url('/course/request.php'); $return = optional_param('return', null, PARAM_ALPHANUMEXT); $categoryid = optional_param('category', null, PARAM_INT); if ($return === 'management') { $url->param('return', $return); - $returnurl = new moodle_url('/course/management.php', array('categoryid' => $CFG->defaultrequestcategory)); + $returnurl = new url('/course/management.php', ['categoryid' => $CFG->defaultrequestcategory]); } else { - $returnurl = new moodle_url('/course/index.php'); + $returnurl = new url('/course/index.php'); } $PAGE->set_url($url); @@ -43,10 +47,10 @@ $PAGE->set_url($url); // Check permissions. require_login(null, false); if (isguestuser()) { - throw new \moodle_exception('guestsarenotallowed', '', $returnurl); + throw new moodle_exception('guestsarenotallowed', '', $returnurl); } if (empty($CFG->enablecourserequests)) { - throw new \moodle_exception('courserequestdisabled', '', $returnurl); + throw new moodle_exception('courserequestdisabled', '', $returnurl); } if ($CFG->lockrequestcategory) { @@ -66,7 +70,7 @@ require_capability('moodle/course:request', $context); // Set up the form. $data = $categoryid ? (object)['category' => $categoryid] : null; $data = course_request::prepare($data); -$requestform = new course_request_form($url); +$requestform = new request_course_form($url); $requestform->set_data($data); $strtitle = get_string('courserequest'); @@ -76,9 +80,8 @@ $PAGE->set_heading($coursecategory->get_formatted_name()); $PAGE->set_primary_active_tab('home'); // Standard form processing if statement. -if ($requestform->is_cancelled()){ +if ($requestform->is_cancelled()) { redirect($returnurl); - } else if ($data = $requestform->get_data()) { $request = course_request::create($data); @@ -86,7 +89,7 @@ if ($requestform->is_cancelled()){ notice(get_string('courserequestsuccess'), $returnurl); } -$categoryurl = new moodle_url('/course/index.php'); +$categoryurl = new url('/course/index.php'); if ($categoryid) { $categoryurl->param('categoryid', $categoryid); } diff --git a/public/course/request_form.php b/public/course/request_form.php deleted file mode 100644 index ede417d9d65..00000000000 --- a/public/course/request_form.php +++ /dev/null @@ -1,153 +0,0 @@ -libdir.'/formslib.php'); - -/** - * A form for a user to request a course. - */ -class course_request_form extends moodleform { - function definition() { - global $CFG, $DB, $USER; - - $mform =& $this->_form; - - if ($pending = $DB->get_records('course_request', array('requester' => $USER->id))) { - $mform->addElement('header', 'pendinglist', get_string('coursespending')); - $list = array(); - foreach ($pending as $cp) { - $list[] = format_string($cp->fullname); - } - $list = implode(', ', $list); - $mform->addElement('static', 'pendingcourses', get_string('courses'), $list); - } - - $mform->addElement('header','coursedetails', get_string('courserequestdetails')); - - $mform->addElement('text', 'fullname', get_string('fullnamecourse'), - ['maxlength' => \core_course\constants::FULLNAME_MAXIMUM_LENGTH, 'size' => 50]); - $mform->addHelpButton('fullname', 'fullnamecourse'); - $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); - $mform->setType('fullname', PARAM_TEXT); - - $mform->addElement('text', 'shortname', get_string('shortnamecourse'), - ['maxlength' => \core_course\constants::SHORTNAME_MAXIMUM_LENGTH, 'size' => 20]); - $mform->addHelpButton('shortname', 'shortnamecourse'); - $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); - $mform->setType('shortname', PARAM_TEXT); - - if (empty($CFG->lockrequestcategory)) { - $displaylist = core_course_category::make_categories_list('moodle/course:request'); - $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist); - $mform->addRule('category', null, 'required', null, 'client'); - $mform->setDefault('category', $CFG->defaultrequestcategory); - $mform->addHelpButton('category', 'coursecategory'); - } - - $mform->addElement('editor', 'summary_editor', get_string('summary'), null, course_request::summary_editor_options()); - $mform->addHelpButton('summary_editor', 'coursesummary'); - $mform->setType('summary_editor', PARAM_RAW); - - $mform->addElement('header','requestreason', get_string('courserequestreason')); - - $mform->addElement('textarea', 'reason', get_string('courserequestsupport'), array('rows'=>'15', 'cols'=>'50')); - $mform->addRule('reason', get_string('missingreqreason'), 'required', null, 'client'); - $mform->setType('reason', PARAM_TEXT); - - $this->add_action_buttons(true, get_string('requestcourse')); - } - - function validation($data, $files) { - global $DB; - - $errors = parent::validation($data, $files); - $foundcourses = null; - $foundreqcourses = null; - - if (!empty($data['shortname'])) { - $foundcourses = $DB->get_records('course', array('shortname'=>$data['shortname'])); - $foundreqcourses = $DB->get_records('course_request', array('shortname'=>$data['shortname'])); - } - if (!empty($foundreqcourses)) { - if (!empty($foundcourses)) { - $foundcourses = array_merge($foundcourses, $foundreqcourses); - } else { - $foundcourses = $foundreqcourses; - } - } - - if (!empty($foundcourses)) { - foreach ($foundcourses as $foundcourse) { - if (!empty($foundcourse->requester)) { - $pending = 1; - $foundcoursenames[] = $foundcourse->fullname.' [*]'; - } else { - $foundcoursenames[] = $foundcourse->fullname; - } - } - $foundcoursenamestring = implode(',', $foundcoursenames); - - $errors['shortname'] = get_string('shortnametaken', '', $foundcoursenamestring); - if (!empty($pending)) { - $errors['shortname'] .= get_string('starpending'); - } - } - - return $errors; - } -} - -/** - * A form for an administrator to reject a course request. - */ -class reject_request_form extends moodleform { - function definition() { - $mform =& $this->_form; - - $mform->addElement('hidden', 'reject', 0); - $mform->setType('reject', PARAM_INT); - - $mform->addElement('header','coursedetails', get_string('coursereasonforrejecting')); - - $mform->addElement('textarea', 'rejectnotice', get_string('coursereasonforrejectingemail'), array('rows'=>'15', 'cols'=>'50')); - $mform->addRule('rejectnotice', get_string('missingreqreason'), 'required', null, 'client'); - $mform->setType('rejectnotice', PARAM_TEXT); - - $this->add_action_buttons(true, get_string('reject')); - } -} - diff --git a/public/course/tests/courserequest_test.php b/public/course/tests/course_request_test.php similarity index 52% rename from public/course/tests/courserequest_test.php rename to public/course/tests/course_request_test.php index 01f97e9b481..005370ad4f9 100644 --- a/public/course/tests/courserequest_test.php +++ b/public/course/tests/course_request_test.php @@ -16,23 +16,20 @@ namespace core_course; -use course_request; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; -require_once($CFG->dirroot.'/course/lib.php'); +use core\context\system as context_system; +use core\context\coursecat as context_coursecat; +use core_course_category; /** - * Course request related unit tests + * Tests for course_request class. * * @package core_course * @category test - * @copyright 2012 Frédéric Massart + * @copyright Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -final class courserequest_test extends \advanced_testcase { - +#[\PHPUnit\Framework\Attributes\CoversClass(course_request::class)] +final class course_request_test extends \advanced_testcase { public function test_create_request(): void { global $DB, $USER; $this->resetAfterTest(true); @@ -95,8 +92,12 @@ final class courserequest_test extends \advanced_testcase { // Create a user and allow course requests for him. $requester = $this->getDataGenerator()->create_user(); $roleid = create_role('Course requestor role', 'courserequestor', ''); - assign_capability('moodle/course:request', CAP_ALLOW, $roleid, - \context_system::instance()->id); + assign_capability( + 'moodle/course:request', + CAP_ALLOW, + $roleid, + \context_system::instance()->id + ); role_assign($roleid, $requester->id, \context_system::instance()->id); accesslib_clear_all_caches_for_unit_testing(); @@ -115,7 +116,7 @@ final class courserequest_test extends \advanced_testcase { $id = $cr->approve(); $this->assertCount(1, $sink->get_messages_by_component_and_type('core', 'courserequestapproved')); $sink->close(); - $course = $DB->get_record('course', array('id' => $id)); + $course = $DB->get_record('course', ['id' => $id]); $this->assertEquals($data->fullname, $course->fullname); $this->assertEquals($data->shortname, $course->shortname); $this->assertEquals($data->summary_editor['text'], $course->summary); @@ -135,7 +136,7 @@ final class courserequest_test extends \advanced_testcase { $id = $cr->approve(); $this->assertCount(1, $sink->get_messages_by_component_and_type('core', 'courserequestapproved')); $sink->close(); - $course = $DB->get_record('course', array('id' => $id)); + $course = $DB->get_record('course', ['id' => $id]); $this->assertEquals($data->category, $course->category); } @@ -152,8 +153,12 @@ final class courserequest_test extends \advanced_testcase { // Create a user and allow course requests for him. $requester = $this->getDataGenerator()->create_user(); $roleid = create_role('Course requestor role', 'courserequestor', ''); - assign_capability('moodle/course:request', CAP_ALLOW, $roleid, - \context_system::instance()->id); + assign_capability( + 'moodle/course:request', + CAP_ALLOW, + $roleid, + \context_system::instance()->id + ); role_assign($roleid, $requester->id, \context_system::instance()->id); accesslib_clear_all_caches_for_unit_testing(); @@ -166,13 +171,133 @@ final class courserequest_test extends \advanced_testcase { $this->setUser($requester); $cr = course_request::create($data); - $this->assertTrue($DB->record_exists('course_request', array('id' => $cr->id))); + $this->assertTrue($DB->record_exists('course_request', ['id' => $cr->id])); $this->setAdminUser(); $sink = $this->redirectMessages(); $cr->reject('Sorry!'); - $this->assertFalse($DB->record_exists('course_request', array('id' => $cr->id))); + $this->assertFalse($DB->record_exists('course_request', ['id' => $cr->id])); $this->assertCount(1, $sink->get_messages()); $sink->close(); } + + /** + * Tests for the course_request::can_request + */ + public function test_can_request_course(): void { + global $CFG, $DB; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $cat1 = $CFG->defaultrequestcategory; + $cat2 = $this->getDataGenerator()->create_category()->id; + $cat3 = $this->getDataGenerator()->create_category()->id; + $context1 = context_coursecat::instance($cat1); + $context2 = context_coursecat::instance($cat2); + $context3 = context_coursecat::instance($cat3); + $this->setUser($user); + + // By default users don't have capability to request courses. + $this->assertFalse(course_request::can_request(context_system::instance())); + $this->assertFalse(course_request::can_request($context1)); + $this->assertFalse(course_request::can_request($context2)); + $this->assertFalse(course_request::can_request($context3)); + + // Allow for the 'user' role the capability to request courses. + $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']); + assign_capability( + 'moodle/course:request', + CAP_ALLOW, + $userroleid, + context_system::instance()->id + ); + accesslib_clear_all_caches_for_unit_testing(); + + // Lock category selection. + $CFG->lockrequestcategory = 1; + + // Now user can only request course in the default category or in system context. + $this->assertTrue(course_request::can_request(context_system::instance())); + $this->assertTrue(course_request::can_request($context1)); + $this->assertFalse(course_request::can_request($context2)); + $this->assertFalse(course_request::can_request($context3)); + + // Enable category selection. User can request course anywhere. + $CFG->lockrequestcategory = 0; + $this->assertTrue(course_request::can_request(context_system::instance())); + $this->assertTrue(course_request::can_request($context1)); + $this->assertTrue(course_request::can_request($context2)); + $this->assertTrue(course_request::can_request($context3)); + + // Remove cap from cat2. + $roleid = create_role('Test role', 'testrole', 'Test role description'); + assign_capability( + 'moodle/course:request', + CAP_PROHIBIT, + $roleid, + $context2->id, + true + ); + role_assign($roleid, $user->id, $context2->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->assertTrue(course_request::can_request(context_system::instance())); + $this->assertTrue(course_request::can_request($context1)); + $this->assertFalse(course_request::can_request($context2)); + $this->assertTrue(course_request::can_request($context3)); + + // Disable course request functionality. + $CFG->enablecourserequests = false; + $this->assertFalse(course_request::can_request(context_system::instance())); + $this->assertFalse(course_request::can_request($context1)); + $this->assertFalse(course_request::can_request($context2)); + $this->assertFalse(course_request::can_request($context3)); + } + + /** + * Tests for the course_request::can_approve + */ + public function test_can_approve_course_request(): void { + global $CFG; + $this->resetAfterTest(); + + $requestor = $this->getDataGenerator()->create_user(); + $user = $this->getDataGenerator()->create_user(); + $cat1 = $CFG->defaultrequestcategory; + $cat2 = $this->getDataGenerator()->create_category()->id; + $cat3 = $this->getDataGenerator()->create_category()->id; + + // Enable course requests. Default 'user' role has capability to request courses. + $CFG->enablecourserequests = true; + $CFG->lockrequestcategory = 0; + $this->setUser($requestor); + $requestdata = ['summary_editor' => ['text' => '', 'format' => 0], 'name' => 'Req', 'reason' => 'test']; + $request1 = course_request::create((object)($requestdata)); + $request2 = course_request::create((object)($requestdata + ['category' => $cat2])); + $request3 = course_request::create((object)($requestdata + ['category' => $cat3])); + + $this->setUser($user); + // Add capability to approve courses. + $roleid = create_role('Test role', 'testrole', 'Test role description'); + assign_capability( + 'moodle/site:approvecourse', + CAP_ALLOW, + $roleid, + context_system::instance()->id, + true + ); + role_assign($roleid, $user->id, context_coursecat::instance($cat2)->id); + accesslib_clear_all_caches_for_unit_testing(); + + $this->assertFalse($request1->can_approve()); + $this->assertTrue($request2->can_approve()); + $this->assertFalse($request3->can_approve()); + + // Delete category where course was requested. Now only site-wide manager can approve it. + core_course_category::get($cat2, MUST_EXIST, true)->delete_full(false); + $this->assertFalse($request2->can_approve()); + + $this->setAdminUser(); + $this->assertTrue($request2->can_approve()); + } } diff --git a/public/course/tests/courselib_test.php b/public/course/tests/courselib_test.php index de3522b7f54..379d811cdb8 100644 --- a/public/course/tests/courselib_test.php +++ b/public/course/tests/courselib_test.php @@ -35,7 +35,6 @@ use core_external; use core_tag_index_builder; use core_tag_tag; use course_capability_assignment; -use course_request; use core_course_category; use enrol_imsenterprise\imsenterprise_test; use core_external\external_api; @@ -7275,112 +7274,6 @@ final class courselib_test extends advanced_testcase { $this->assertEquals($expected, course_modules_pending_deletion($course->id, $gradable)); } - /** - * Tests for the course_request::can_request - */ - public function test_can_request_course(): void { - global $CFG, $DB; - $this->resetAfterTest(); - - $user = $this->getDataGenerator()->create_user(); - $cat1 = $CFG->defaultrequestcategory; - $cat2 = $this->getDataGenerator()->create_category()->id; - $cat3 = $this->getDataGenerator()->create_category()->id; - $context1 = context_coursecat::instance($cat1); - $context2 = context_coursecat::instance($cat2); - $context3 = context_coursecat::instance($cat3); - $this->setUser($user); - - // By default users don't have capability to request courses. - $this->assertFalse(course_request::can_request(context_system::instance())); - $this->assertFalse(course_request::can_request($context1)); - $this->assertFalse(course_request::can_request($context2)); - $this->assertFalse(course_request::can_request($context3)); - - // Allow for the 'user' role the capability to request courses. - $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']); - assign_capability('moodle/course:request', CAP_ALLOW, $userroleid, - context_system::instance()->id); - accesslib_clear_all_caches_for_unit_testing(); - - // Lock category selection. - $CFG->lockrequestcategory = 1; - - // Now user can only request course in the default category or in system context. - $this->assertTrue(course_request::can_request(context_system::instance())); - $this->assertTrue(course_request::can_request($context1)); - $this->assertFalse(course_request::can_request($context2)); - $this->assertFalse(course_request::can_request($context3)); - - // Enable category selection. User can request course anywhere. - $CFG->lockrequestcategory = 0; - $this->assertTrue(course_request::can_request(context_system::instance())); - $this->assertTrue(course_request::can_request($context1)); - $this->assertTrue(course_request::can_request($context2)); - $this->assertTrue(course_request::can_request($context3)); - - // Remove cap from cat2. - $roleid = create_role('Test role', 'testrole', 'Test role description'); - assign_capability('moodle/course:request', CAP_PROHIBIT, $roleid, - $context2->id, true); - role_assign($roleid, $user->id, $context2->id); - accesslib_clear_all_caches_for_unit_testing(); - - $this->assertTrue(course_request::can_request(context_system::instance())); - $this->assertTrue(course_request::can_request($context1)); - $this->assertFalse(course_request::can_request($context2)); - $this->assertTrue(course_request::can_request($context3)); - - // Disable course request functionality. - $CFG->enablecourserequests = false; - $this->assertFalse(course_request::can_request(context_system::instance())); - $this->assertFalse(course_request::can_request($context1)); - $this->assertFalse(course_request::can_request($context2)); - $this->assertFalse(course_request::can_request($context3)); - } - - /** - * Tests for the course_request::can_approve - */ - public function test_can_approve_course_request(): void { - global $CFG; - $this->resetAfterTest(); - - $requestor = $this->getDataGenerator()->create_user(); - $user = $this->getDataGenerator()->create_user(); - $cat1 = $CFG->defaultrequestcategory; - $cat2 = $this->getDataGenerator()->create_category()->id; - $cat3 = $this->getDataGenerator()->create_category()->id; - - // Enable course requests. Default 'user' role has capability to request courses. - $CFG->enablecourserequests = true; - $CFG->lockrequestcategory = 0; - $this->setUser($requestor); - $requestdata = ['summary_editor' => ['text' => '', 'format' => 0], 'name' => 'Req', 'reason' => 'test']; - $request1 = course_request::create((object)($requestdata)); - $request2 = course_request::create((object)($requestdata + ['category' => $cat2])); - $request3 = course_request::create((object)($requestdata + ['category' => $cat3])); - - $this->setUser($user); - // Add capability to approve courses. - $roleid = create_role('Test role', 'testrole', 'Test role description'); - assign_capability('moodle/site:approvecourse', CAP_ALLOW, $roleid, - context_system::instance()->id, true); - role_assign($roleid, $user->id, context_coursecat::instance($cat2)->id); - accesslib_clear_all_caches_for_unit_testing(); - - $this->assertFalse($request1->can_approve()); - $this->assertTrue($request2->can_approve()); - $this->assertFalse($request3->can_approve()); - - // Delete category where course was requested. Now only site-wide manager can approve it. - core_course_category::get($cat2, MUST_EXIST, true)->delete_full(false); - $this->assertFalse($request2->can_approve()); - - $this->setAdminUser(); - $this->assertTrue($request2->can_approve()); - } - /** * Test the course allowed module method. */ diff --git a/public/lib/db/legacyclasses.php b/public/lib/db/legacyclasses.php index 9fac6b6fca9..1723e26708f 100644 --- a/public/lib/db/legacyclasses.php +++ b/public/lib/db/legacyclasses.php @@ -287,6 +287,16 @@ $legacyclasses = [ 'dataformat_export_format.php', ], + // Course drag-and-drop upload system. + \dndupload_handler::class => [ + 'core_course', + 'dndupload_handler.php', + ], + \dndupload_ajax_processor::class => [ + 'core_course', + 'dndupload_ajax_processor.php', + ], + // The progress_trace classes. \combined_progress_trace::class => 'output/progress_trace/combined_progress_trace.php', \error_log_progress_trace::class => 'output/progress_trace/error_log_progress_trace.php', @@ -346,4 +356,8 @@ $legacyclasses = [ 'core_comment', 'comment_exception.php', ], + \course_request::class => [ + 'core_course', + 'course_request.php', + ], ];