156c6f69ac
Upgrade step to ensure the default value for this field in the schema matches the default value used in the interface itself.
472 lines
17 KiB
PHP
472 lines
17 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
namespace core_course;
|
|
|
|
use core\{clock, di};
|
|
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')) {
|
|
$category = \core_course_category::get($data->category);
|
|
|
|
$a = new stdClass();
|
|
$a->link = "$CFG->wwwroot/course/pending.php";
|
|
$a->user = fullname($USER);
|
|
$a->shortname = format_string($data->shortname, true, ['context' => $category->get_context()]);
|
|
$a->fullname = format_string($data->fullname, true, ['context' => $category->get_context()]);
|
|
$a->category = $category->get_formatted_name();
|
|
$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->showactivitydates = $courseconfig->showactivitydates;
|
|
$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(di::get(clock::class)->time());
|
|
if ($courseconfig->courseenddateenabled) {
|
|
$data->enddate = $data->startdate + $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);
|