Файловый менеджер - Редактировать - /home/harasnat/www/mf/lib.php.tar
Назад
home/harasnat/www/learning/rating/lib.php 0000604 00000150020 15062104401 0014446 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A class representing a single rating and containing some static methods for manipulating ratings * * @package core_rating * @subpackage rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('RATING_UNSET_RATING', -999); define ('RATING_AGGREGATE_NONE', 0); // No ratings. define ('RATING_AGGREGATE_AVERAGE', 1); define ('RATING_AGGREGATE_COUNT', 2); define ('RATING_AGGREGATE_MAXIMUM', 3); define ('RATING_AGGREGATE_MINIMUM', 4); define ('RATING_AGGREGATE_SUM', 5); define ('RATING_DEFAULT_SCALE', 5); /** * The rating class represents a single rating by a single user * * @package core_rating * @category rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class rating implements renderable { /** * @var context The context in which this rating exists */ public $context; /** * @var string The component using ratings. For example "mod_forum" */ public $component; /** * @var string The rating area to associate this rating with * This allows a plugin to rate more than one thing by specifying different rating areas */ public $ratingarea = null; /** * @var int The id of the item (forum post, glossary item etc) being rated */ public $itemid; /** * @var int The id scale (1-5, 0-100) that was in use when the rating was submitted */ public $scaleid; /** * @var int The id of the user who submitted the rating */ public $userid; /** * @var stdclass settings for this rating. Necessary to render the rating. */ public $settings; /** * @var int The Id of this rating within the rating table. This is only set if the rating already exists */ public $id = null; /** * @var int The aggregate of the combined ratings for the associated item. This is only set if the rating already exists */ public $aggregate = null; /** * @var int The total number of ratings for the associated item. This is only set if the rating already exists */ public $count = 0; /** * @var int The rating the associated user gave the associated item. This is only set if the rating already exists */ public $rating = null; /** * @var int The time the associated item was created */ public $itemtimecreated = null; /** * @var int The id of the user who submitted the rating */ public $itemuserid = null; /** * Constructor. * * @param stdClass $options { * context => context context to use for the rating [required] * component => component using ratings ie mod_forum [required] * ratingarea => ratingarea to associate this rating with [required] * itemid => int the id of the associated item (forum post, glossary item etc) [required] * scaleid => int The scale in use when the rating was submitted [required] * userid => int The id of the user who submitted the rating [required] * settings => Settings for the rating object [optional] * id => The id of this rating (if the rating is from the db) [optional] * aggregate => The aggregate for the rating [optional] * count => The number of ratings [optional] * rating => The rating given by the user [optional] * } */ public function __construct($options) { $this->context = $options->context; $this->component = $options->component; $this->ratingarea = $options->ratingarea; $this->itemid = $options->itemid; $this->scaleid = $options->scaleid; $this->userid = $options->userid; if (isset($options->settings)) { $this->settings = $options->settings; } if (isset($options->id)) { $this->id = $options->id; } if (isset($options->aggregate)) { $this->aggregate = $options->aggregate; } if (isset($options->count)) { $this->count = $options->count; } if (isset($options->rating)) { $this->rating = $options->rating; } } /** * Update this rating in the database * * @param int $rating the integer value of this rating */ public function update_rating($rating) { global $DB; $time = time(); $data = new stdClass; $data->rating = $rating; $data->timemodified = $time; $item = new stdclass(); $item->id = $this->itemid; $items = array($item); $ratingoptions = new stdClass; $ratingoptions->context = $this->context; $ratingoptions->component = $this->component; $ratingoptions->ratingarea = $this->ratingarea; $ratingoptions->items = $items; $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE; // We dont actually care what aggregation method is applied. $ratingoptions->scaleid = $this->scaleid; $ratingoptions->userid = $this->userid; $rm = new rating_manager(); $items = $rm->get_ratings($ratingoptions); $firstitem = $items[0]->rating; if (empty($firstitem->id)) { // Insert a new rating. $data->contextid = $this->context->id; $data->component = $this->component; $data->ratingarea = $this->ratingarea; $data->rating = $rating; $data->scaleid = $this->scaleid; $data->userid = $this->userid; $data->itemid = $this->itemid; $data->timecreated = $time; $data->timemodified = $time; $DB->insert_record('rating', $data); } else { // Update the rating. $data->id = $firstitem->id; $DB->update_record('rating', $data); } } /** * Retreive the integer value of this rating * * @return int the integer value of this rating object */ public function get_rating() { return $this->rating; } /** * Returns this ratings aggregate value as a string. * * @return string ratings aggregate value */ public function get_aggregate_string() { $aggregate = $this->aggregate; $method = $this->settings->aggregationmethod; // Only display aggregate if aggregation method isn't COUNT. $aggregatestr = ''; if (is_numeric($aggregate) && $method != RATING_AGGREGATE_COUNT) { if ($method != RATING_AGGREGATE_SUM && !$this->settings->scale->isnumeric) { // Round aggregate as we're using it as an index. $aggregatestr .= $this->settings->scale->scaleitems[round($aggregate)]; } else { // Aggregation is SUM or the scale is numeric. $aggregatestr .= round($aggregate, 1); } } return $aggregatestr; } /** * Returns true if the user is able to rate this rating object * * @param int $userid Current user assumed if left empty * @return bool true if the user is able to rate this rating object */ public function user_can_rate($userid = null) { if (empty($userid)) { global $USER; $userid = $USER->id; } // You can't rate your item. if ($this->itemuserid == $userid) { return false; } // You can't rate if you don't have the system cap. if (!$this->settings->permissions->rate) { return false; } // You can't rate if you don't have the plugin cap. if (!$this->settings->pluginpermissions->rate) { return false; } // You can't rate if the item was outside of the assessment times. $timestart = $this->settings->assesstimestart; $timefinish = $this->settings->assesstimefinish; $timecreated = $this->itemtimecreated; if (!empty($timestart) && !empty($timefinish) && ($timecreated < $timestart || $timecreated > $timefinish)) { return false; } return true; } /** * Returns true if the user is able to view the aggregate for this rating object. * * @param int|null $userid If left empty the current user is assumed. * @return bool true if the user is able to view the aggregate for this rating object */ public function user_can_view_aggregate($userid = null) { if (empty($userid)) { global $USER; $userid = $USER->id; } // If the item doesnt belong to anyone or its another user's items and they can see the aggregate on items they don't own. // Note that viewany doesnt mean you can see the aggregate or ratings of your own items. if ((empty($this->itemuserid) or $this->itemuserid != $userid) && $this->settings->permissions->viewany && $this->settings->pluginpermissions->viewany ) { return true; } // If its the current user's item and they have permission to view the aggregate on their own items. if ($this->itemuserid == $userid && $this->settings->permissions->view && $this->settings->pluginpermissions->view) { return true; } return false; } /** * Returns a URL to view all of the ratings for the item this rating is for. * * If this is a rating of a post then this URL will take the user to a page that shows all of the ratings for the post * (this one included). * * @param bool $popup whether of not the URL should be loaded in a popup * @return moodle_url URL to view all of the ratings for the item this rating is for. */ public function get_view_ratings_url($popup = false) { $attributes = array( 'contextid' => $this->context->id, 'component' => $this->component, 'ratingarea' => $this->ratingarea, 'itemid' => $this->itemid, 'scaleid' => $this->settings->scale->id ); if ($popup) { $attributes['popup'] = 1; } return new moodle_url('/rating/index.php', $attributes); } /** * Returns a URL that can be used to rate the associated item. * * @param int|null $rating The rating to give the item, if null then no rating param is added. * @param moodle_url|string $returnurl The URL to return to. * @return moodle_url can be used to rate the associated item. */ public function get_rate_url($rating = null, $returnurl = null) { if (empty($returnurl)) { if (!empty($this->settings->returnurl)) { $returnurl = $this->settings->returnurl; } else { global $PAGE; $returnurl = $PAGE->url; } } $args = array( 'contextid' => $this->context->id, 'component' => $this->component, 'ratingarea' => $this->ratingarea, 'itemid' => $this->itemid, 'scaleid' => $this->settings->scale->id, 'returnurl' => $returnurl, 'rateduserid' => $this->itemuserid, 'aggregation' => $this->settings->aggregationmethod, 'sesskey' => sesskey() ); if (!empty($rating)) { $args['rating'] = $rating; } $url = new moodle_url('/rating/rate.php', $args); return $url; } } // End rating class definition. /** * The rating_manager class provides the ability to retrieve sets of ratings from the database * * @package core_rating * @category rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class rating_manager { /** * @var array An array of calculated scale options to save us generating them for each request. */ protected $scales = array(); /** * Delete one or more ratings. Specify either a rating id, an item id or just the context id. * * @global moodle_database $DB * @param stdClass $options { * contextid => int the context in which the ratings exist [required] * ratingid => int the id of an individual rating to delete [optional] * userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional] * itemid => int delete all ratings attached to this item [optional] * component => string The component to delete ratings from [optional] * ratingarea => string The ratingarea to delete ratings from [optional] * } */ public function delete_ratings($options) { global $DB; if (empty($options->contextid)) { throw new coding_exception('The context option is a required option when deleting ratings.'); } $conditions = array('contextid' => $options->contextid); $possibleconditions = array( 'ratingid' => 'id', 'userid' => 'userid', 'itemid' => 'itemid', 'component' => 'component', 'ratingarea' => 'ratingarea' ); foreach ($possibleconditions as $option => $field) { if (isset($options->{$option})) { $conditions[$field] = $options->{$option}; } } $DB->delete_records('rating', $conditions); } /** * Returns an array of ratings for a given item (forum post, glossary entry etc). * * This returns all users ratings for a single item * * @param stdClass $options { * context => context the context in which the ratings exists [required] * component => component using ratings ie mod_forum [required] * ratingarea => ratingarea to associate this rating with [required] * itemid => int the id of the associated item (forum post, glossary item etc) [required] * sort => string SQL sort by clause [optional] * } * @return array an array of ratings */ public function get_all_ratings_for_item($options) { global $DB; if (!isset($options->context)) { throw new coding_exception('The context option is a required option when getting ratings for an item.'); } if (!isset($options->itemid)) { throw new coding_exception('The itemid option is a required option when getting ratings for an item.'); } if (!isset($options->component)) { throw new coding_exception('The component option is now a required option when getting ratings for an item.'); } if (!isset($options->ratingarea)) { throw new coding_exception('The ratingarea option is now a required option when getting ratings for an item.'); } $sortclause = ''; if (!empty($options->sort)) { $sortclause = "ORDER BY $options->sort"; } $params = array( 'contextid' => $options->context->id, 'itemid' => $options->itemid, 'component' => $options->component, 'ratingarea' => $options->ratingarea, ); $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, r.component, r.ratingarea, $userfields FROM {rating} r LEFT JOIN {user} u ON r.userid = u.id WHERE r.contextid = :contextid AND r.itemid = :itemid AND r.component = :component AND r.ratingarea = :ratingarea {$sortclause}"; return $DB->get_records_sql($sql, $params); } /** * Adds rating objects to an array of items (forum posts, glossary entries etc). Rating objects are available at $item->rating * * @param stdClass $options { * context => context the context in which the ratings exists [required] * component => the component name ie mod_forum [required] * ratingarea => the ratingarea we are interested in [required] * items => array items like forum posts or glossary items. Each item needs an 'id' ie $items[0]->id [required] * aggregate => int aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required] * scaleid => int the scale from which the user can select a rating [required] * userid => int the id of the current user [optional] * returnurl => string the url to return the user to after submitting a rating. Null for ajax requests [optional] * assesstimestart => int only allow rating of items created after this timestamp [optional] * assesstimefinish => int only allow rating of items created before this timestamp [optional] * @return array the array of items with their ratings attached at $items[0]->rating */ public function get_ratings($options) { global $DB, $USER; if (!isset($options->context)) { throw new coding_exception('The context option is a required option when getting ratings.'); } if (!isset($options->component)) { throw new coding_exception('The component option is a required option when getting ratings.'); } if (!isset($options->ratingarea)) { throw new coding_exception('The ratingarea option is a required option when getting ratings.'); } if (!isset($options->scaleid)) { throw new coding_exception('The scaleid option is a required option when getting ratings.'); } if (!isset($options->items)) { throw new coding_exception('The items option is a required option when getting ratings.'); } else if (empty($options->items)) { return array(); } if (!isset($options->aggregate)) { throw new coding_exception('The aggregate option is a required option when getting ratings.'); } else if ($options->aggregate == RATING_AGGREGATE_NONE) { // Ratings are not enabled. return $options->items; } // Ensure average aggregation returns float. $aggregatestr = $this->get_aggregation_method($options->aggregate); $aggregatefield = 'r.rating'; if ($aggregatestr === 'AVG') { $aggregatefield = "1.0 * {$aggregatefield}"; } // Default the userid to the current user if it is not set. if (empty($options->userid)) { $userid = $USER->id; } else { $userid = $options->userid; } // Get the item table name, the item id field, and the item user field for the given rating item // from the related component. list($type, $name) = core_component::normalize_component($options->component); $default = array(null, 'id', 'userid'); list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type, $name, 'rating', 'get_item_fields', array($options), $default); // Create an array of item IDs. $itemids = array(); foreach ($options->items as $item) { $itemids[] = $item->{$itemidcol}; } // Get the items from the database. list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); $params['contextid'] = $options->context->id; $params['userid'] = $userid; $params['component'] = $options->component; $params['ratingarea'] = $options->ratingarea; $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating FROM {rating} r WHERE r.userid = :userid AND r.contextid = :contextid AND r.itemid {$itemidtest} AND r.component = :component AND r.ratingarea = :ratingarea ORDER BY r.itemid"; $userratings = $DB->get_records_sql($sql, $params); $sql = "SELECT r.itemid, {$aggregatestr}({$aggregatefield}) AS aggrrating, COUNT(r.rating) AS numratings FROM {rating} r WHERE r.contextid = :contextid AND r.itemid {$itemidtest} AND r.component = :component AND r.ratingarea = :ratingarea GROUP BY r.itemid, r.component, r.ratingarea, r.contextid ORDER BY r.itemid"; $aggregateratings = $DB->get_records_sql($sql, $params); $ratingoptions = new stdClass; $ratingoptions->context = $options->context; $ratingoptions->component = $options->component; $ratingoptions->ratingarea = $options->ratingarea; $ratingoptions->settings = $this->generate_rating_settings_object($options); foreach ($options->items as $item) { $founduserrating = false; foreach ($userratings as $userrating) { // Look for an existing rating from this user of this item. if ($item->{$itemidcol} == $userrating->itemid) { // Note: rec->scaleid = the id of scale at the time the rating was submitted. // It may be different from the current scale id. $ratingoptions->scaleid = $userrating->scaleid; $ratingoptions->userid = $userrating->userid; $ratingoptions->id = $userrating->id; $ratingoptions->rating = min($userrating->usersrating, $ratingoptions->settings->scale->max); $founduserrating = true; break; } } if (!$founduserrating) { $ratingoptions->scaleid = null; $ratingoptions->userid = null; $ratingoptions->id = null; $ratingoptions->rating = null; } if (array_key_exists($item->{$itemidcol}, $aggregateratings)) { $rec = $aggregateratings[$item->{$itemidcol}]; $ratingoptions->itemid = $item->{$itemidcol}; $ratingoptions->aggregate = min($rec->aggrrating, $ratingoptions->settings->scale->max); $ratingoptions->count = $rec->numratings; } else { $ratingoptions->itemid = $item->{$itemidcol}; $ratingoptions->aggregate = null; $ratingoptions->count = 0; } $rating = new rating($ratingoptions); $rating->itemtimecreated = $this->get_item_time_created($item); if (!empty($item->{$itemuseridcol})) { $rating->itemuserid = $item->{$itemuseridcol}; } $item->rating = $rating; } return $options->items; } /** * Generates a rating settings object based upon the options it is provided. * * @param stdClass $options { * context => context the context in which the ratings exists [required] * component => string The component the items belong to [required] * ratingarea => string The ratingarea the items belong to [required] * aggregate => int Aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required] * scaleid => int the scale from which the user can select a rating [required] * returnurl => string the url to return the user to after submitting a rating. Null for ajax requests [optional] * assesstimestart => int only allow rating of items created after this timestamp [optional] * assesstimefinish => int only allow rating of items created before this timestamp [optional] * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional] * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional] * } * @return stdClass rating settings object */ protected function generate_rating_settings_object($options) { if (!isset($options->context)) { throw new coding_exception('The context option is a required option when generating a rating settings object.'); } if (!isset($options->component)) { throw new coding_exception('The component option is now a required option when generating a rating settings object.'); } if (!isset($options->ratingarea)) { throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.'); } if (!isset($options->aggregate)) { throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.'); } if (!isset($options->scaleid)) { throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.'); } // Settings that are common to all ratings objects in this context. $settings = new stdClass; $settings->scale = $this->generate_rating_scale_object($options->scaleid); // The scale to use now. $settings->aggregationmethod = $options->aggregate; $settings->assesstimestart = null; $settings->assesstimefinish = null; // Collect options into the settings object. if (!empty($options->assesstimestart)) { $settings->assesstimestart = $options->assesstimestart; } if (!empty($options->assesstimefinish)) { $settings->assesstimefinish = $options->assesstimefinish; } if (!empty($options->returnurl)) { $settings->returnurl = $options->returnurl; } // Check site capabilities. $settings->permissions = new stdClass; // Can view the aggregate of ratings of their own items. $settings->permissions->view = has_capability('moodle/rating:view', $options->context); // Can view the aggregate of ratings of other people's items. $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context); // Can view individual ratings. $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context); // Can submit ratings. $settings->permissions->rate = has_capability('moodle/rating:rate', $options->context); // Check module capabilities // This is mostly for backwards compatability with old modules that previously implemented their own ratings. $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, $options->component, $options->ratingarea); $settings->pluginpermissions = new stdClass; $settings->pluginpermissions->view = $pluginpermissionsarray['view']; $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany']; $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall']; $settings->pluginpermissions->rate = $pluginpermissionsarray['rate']; return $settings; } /** * Generates a scale object that can be returned * * @global moodle_database $DB moodle database object * @param int $scaleid scale-type identifier * @return stdClass scale for ratings */ protected function generate_rating_scale_object($scaleid) { global $DB; if (!array_key_exists('s'.$scaleid, $this->scales)) { $scale = new stdClass; $scale->id = $scaleid; $scale->name = null; $scale->courseid = null; $scale->scaleitems = array(); $scale->isnumeric = true; $scale->max = $scaleid; if ($scaleid < 0) { // It is a proper scale (not numeric). $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid))); if ($scalerecord) { // We need to generate an array with string keys starting at 1. $scalearray = explode(',', $scalerecord->scale); $c = count($scalearray); for ($i = 0; $i < $c; $i++) { // Treat index as a string to allow sorting without changing the value. $scale->scaleitems[(string)($i + 1)] = $scalearray[$i]; } krsort($scale->scaleitems); // Have the highest grade scale item appear first. $scale->isnumeric = false; $scale->name = $scalerecord->name; $scale->courseid = $scalerecord->courseid; $scale->max = count($scale->scaleitems); } } else { // Generate an array of values for numeric scales. for ($i = 0; $i <= (int)$scaleid; $i++) { $scale->scaleitems[(string)$i] = $i; } } $this->scales['s'.$scaleid] = $scale; } return $this->scales['s'.$scaleid]; } /** * Gets the time the given item was created * * TODO: MDL-31511 - Find a better solution for this, its not ideal to test for fields really we should be * asking the component the item belongs to what field to look for or even the value we * are looking for. * * @param stdClass $item * @return int|null return null if the created time is unavailable, otherwise return a timestamp */ protected function get_item_time_created($item) { if (!empty($item->created)) { return $item->created; // The forum_posts table has created instead of timecreated. } else if (!empty($item->timecreated)) { return $item->timecreated; } else { return null; } } /** * Returns an array of grades calculated by aggregating item ratings. * * @param stdClass $options { * userid => int the id of the user whose items were rated, NOT the user who submitted ratings. 0 to update all. [required] * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required] * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] * itemtable => int the table containing the items [required] * itemtableusercolum => int the column of the user table containing the item owner's user id [required] * component => The component for the ratings [required] * ratingarea => The ratingarea for the ratings [required] * contextid => int the context in which the rated items exist [optional] * modulename => string the name of the module [optional] * moduleid => int the id of the module instance [optional] * } * @return array the array of the user's grades */ public function get_user_grades($options) { global $DB; $contextid = null; if (!isset($options->component)) { throw new coding_exception('The component option is now a required option when getting user grades from ratings.'); } if (!isset($options->ratingarea)) { throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.'); } // If the calling code doesn't supply a context id we'll have to figure it out. if (!empty($options->contextid)) { $contextid = $options->contextid; } else if (!empty($options->modulename) && !empty($options->moduleid)) { $modulename = $options->modulename; $moduleid = intval($options->moduleid); // Going direct to the db for the context id seems wrong. $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel)"; $sql = "SELECT cm.* $ctxselect FROM {course_modules} cm LEFT JOIN {modules} mo ON mo.id = cm.module LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin WHERE mo.name=:modulename AND m.id=:moduleid"; $params = array('modulename' => $modulename, 'moduleid' => $moduleid, 'contextlevel' => CONTEXT_MODULE); $contextrecord = $DB->get_record_sql($sql, $params, '*', MUST_EXIST); $contextid = $contextrecord->ctxid; } $params = array(); $params['contextid'] = $contextid; $params['component'] = $options->component; $params['ratingarea'] = $options->ratingarea; $itemtable = $options->itemtable; $itemtableusercolumn = $options->itemtableusercolumn; $scaleid = $options->scaleid; // Ensure average aggregation returns float. $aggregationstring = $this->get_aggregation_method($options->aggregationmethod); $aggregationfield = 'r.rating'; if ($aggregationstring === 'AVG') { $aggregationfield = "1.0 * {$aggregationfield}"; } // If userid is not 0 we only want the grade for a single user. $singleuserwhere = ''; if ($options->userid != 0) { $params['userid1'] = intval($options->userid); $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1"; } // MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)". // r.contextid will be null for users who haven't been rated yet. // No longer including users who haven't been rated to reduce memory requirements. $sql = "SELECT u.id as id, u.id AS userid, {$aggregationstring}({$aggregationfield}) AS rawgrade FROM {user} u LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn} LEFT JOIN {rating} r ON r.itemid=i.id WHERE r.contextid = :contextid AND r.component = :component AND r.ratingarea = :ratingarea $singleuserwhere GROUP BY u.id"; $results = $DB->get_records_sql($sql, $params); if ($results) { $scale = null; $max = 0; if ($options->scaleid >= 0) { // Numeric. $max = $options->scaleid; } else { // Custom scales. $scale = $DB->get_record('scale', array('id' => -$options->scaleid)); if ($scale) { $scale = explode(',', $scale->scale); $max = count($scale); } else { debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist'); } } // It could throw off the grading if count and sum returned a rawgrade higher than scale // so to prevent it we review the results and ensure that rawgrade does not exceed the scale. // If it does we set rawgrade = scale (i.e. full credit). foreach ($results as $rid => $result) { if ($options->scaleid >= 0) { // Numeric. if ($result->rawgrade > $options->scaleid) { $results[$rid]->rawgrade = $options->scaleid; } } else { // Scales. if (!empty($scale) && $result->rawgrade > $max) { $results[$rid]->rawgrade = $max; } } } } return $results; } /** * Returns array of aggregate types. Used by ratings. * * @return array aggregate types */ public function get_aggregate_types() { return array (RATING_AGGREGATE_NONE => get_string('aggregatenone', 'rating'), RATING_AGGREGATE_AVERAGE => get_string('aggregateavg', 'rating'), RATING_AGGREGATE_COUNT => get_string('aggregatecount', 'rating'), RATING_AGGREGATE_MAXIMUM => get_string('aggregatemax', 'rating'), RATING_AGGREGATE_MINIMUM => get_string('aggregatemin', 'rating'), RATING_AGGREGATE_SUM => get_string('aggregatesum', 'rating')); } /** * Converts an aggregation method constant into something that can be included in SQL * * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE. * @return string an SQL aggregation method */ public function get_aggregation_method($aggregate) { $aggregatestr = null; switch($aggregate){ case RATING_AGGREGATE_AVERAGE: $aggregatestr = 'AVG'; break; case RATING_AGGREGATE_COUNT: $aggregatestr = 'COUNT'; break; case RATING_AGGREGATE_MAXIMUM: $aggregatestr = 'MAX'; break; case RATING_AGGREGATE_MINIMUM: $aggregatestr = 'MIN'; break; case RATING_AGGREGATE_SUM: $aggregatestr = 'SUM'; break; default: $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270. debugging('Incorrect call to get_aggregation_method(), incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER); } return $aggregatestr; } /** * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated * * @param int $contextid The current context id * @param string $component the name of the component that is using ratings ie 'mod_forum' * @param string $ratingarea The area the rating is associated with * @return array rating related permissions */ public function get_plugin_permissions_array($contextid, $component, $ratingarea) { $pluginpermissionsarray = null; // Deny by default. $defaultpluginpermissions = array('rate' => false, 'view' => false, 'viewany' => false, 'viewall' => false); if (!empty($component)) { list($type, $name) = core_component::normalize_component($component); $pluginpermissionsarray = plugin_callback($type, $name, 'rating', 'permissions', array($contextid, $component, $ratingarea), $defaultpluginpermissions); } else { $pluginpermissionsarray = $defaultpluginpermissions; } return $pluginpermissionsarray; } /** * Validates a submitted rating * * @param array $params submitted data * context => object the context in which the rated items exists [required] * component => The component the rating belongs to [required] * ratingarea => The ratingarea the rating is associated with [required] * itemid => int the ID of the object being rated [required] * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] * rating => int the submitted rating * rateduserid => int the id of the user whose items have been rated. 0 to update all. [required] * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional] * @return boolean true if the rating is valid, false if callback not found, throws rating_exception if rating is invalid */ public function check_rating_is_valid($params) { if (!isset($params['context'])) { throw new coding_exception('The context option is a required option when checking rating validity.'); } if (!isset($params['component'])) { throw new coding_exception('The component option is now a required option when checking rating validity'); } if (!isset($params['ratingarea'])) { throw new coding_exception('The ratingarea option is now a required option when checking rating validity'); } if (!isset($params['itemid'])) { throw new coding_exception('The itemid option is now a required option when checking rating validity'); } if (!isset($params['scaleid'])) { throw new coding_exception('The scaleid option is now a required option when checking rating validity'); } if (!isset($params['rateduserid'])) { throw new coding_exception('The rateduserid option is now a required option when checking rating validity'); } list($plugintype, $pluginname) = core_component::normalize_component($params['component']); // This looks for a function like forum_rating_validate() in mod_forum lib.php // wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments. $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null); // If null then the callback does not exist. if ($isvalid === null) { $isvalid = false; debugging('rating validation callback not found for component '. clean_param($component, PARAM_ALPHANUMEXT)); } return $isvalid; } /** * Initialises JavaScript to enable AJAX ratings on the provided page * * @param moodle_page $page * @return true always returns true */ public function initialise_rating_javascript(moodle_page $page) { global $CFG; // Only needs to be initialized once. static $done = false; if ($done) { return true; } $page->requires->js_init_call('M.core_rating.init'); $done = true; return true; } /** * Returns a string that describes the aggregation method that was provided. * * @param string $aggregationmethod * @return string describes the aggregation method that was provided */ public function get_aggregate_label($aggregationmethod) { $aggregatelabel = ''; switch ($aggregationmethod) { case RATING_AGGREGATE_AVERAGE : $aggregatelabel .= get_string("aggregateavg", "rating"); break; case RATING_AGGREGATE_COUNT : $aggregatelabel .= get_string("aggregatecount", "rating"); break; case RATING_AGGREGATE_MAXIMUM : $aggregatelabel .= get_string("aggregatemax", "rating"); break; case RATING_AGGREGATE_MINIMUM : $aggregatelabel .= get_string("aggregatemin", "rating"); break; case RATING_AGGREGATE_SUM : $aggregatelabel .= get_string("aggregatesum", "rating"); break; } $aggregatelabel .= get_string('labelsep', 'langconfig'); return $aggregatelabel; } /** * Adds a new rating * * @param stdClass $cm course module object * @param stdClass $context context object * @param string $component component name * @param string $ratingarea rating area * @param int $itemid the item id * @param int $scaleid the scale id * @param int $userrating the user rating * @param int $rateduserid the rated user id * @param int $aggregationmethod the aggregation method * @since Moodle 3.2 */ public function add_rating($cm, $context, $component, $ratingarea, $itemid, $scaleid, $userrating, $rateduserid, $aggregationmethod) { global $CFG, $DB, $USER; $result = new stdClass; // Check the module rating permissions. // Doing this check here rather than within rating_manager::get_ratings() so we can return a error response. $pluginpermissionsarray = $this->get_plugin_permissions_array($context->id, $component, $ratingarea); if (!$pluginpermissionsarray['rate']) { $result->error = 'ratepermissiondenied'; return $result; } else { $params = array( 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid, 'rating' => $userrating, 'rateduserid' => $rateduserid, 'aggregation' => $aggregationmethod ); if (!$this->check_rating_is_valid($params)) { $result->error = 'ratinginvalid'; return $result; } } // Rating options used to update the rating then retrieve the aggregate. $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->ratingarea = $ratingarea; $ratingoptions->component = $component; $ratingoptions->itemid = $itemid; $ratingoptions->scaleid = $scaleid; $ratingoptions->userid = $USER->id; if ($userrating != RATING_UNSET_RATING) { $rating = new rating($ratingoptions); $rating->update_rating($userrating); } else { // Delete the rating if the user set to "Rate..." $options = new stdClass; $options->contextid = $context->id; $options->component = $component; $options->ratingarea = $ratingarea; $options->userid = $USER->id; $options->itemid = $itemid; $this->delete_ratings($options); } // Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook. // Note that this would need to be done in both rate.php and rate_ajax.php. if ($context->contextlevel == CONTEXT_MODULE) { // Tell the module that its grades have changed. $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance)); if ($modinstance) { $modinstance->cmidnumber = $cm->id; // MDL-12961. $functionname = $cm->modname.'_update_grades'; require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php"); if (function_exists($functionname)) { $functionname($modinstance, $rateduserid); } } } // Object to return to client as JSON. $result->success = true; // Need to retrieve the updated item to get its new aggregate value. $item = new stdClass; $item->id = $itemid; // Most of $ratingoptions variables were previously set. $ratingoptions->items = array($item); $ratingoptions->aggregate = $aggregationmethod; $items = $this->get_ratings($ratingoptions); $firstrating = $items[0]->rating; // See if the user has permission to see the rating aggregate. if ($firstrating->user_can_view_aggregate()) { // For custom scales return text not the value. // This scales weirdness will go away when scales are refactored. $scalearray = null; $aggregatetoreturn = round($firstrating->aggregate, 1); // Output a dash if aggregation method == COUNT as the count is output next to the aggregate anyway. if ($firstrating->settings->aggregationmethod == RATING_AGGREGATE_COUNT or $firstrating->count == 0) { $aggregatetoreturn = ' - '; } else if ($firstrating->settings->scale->id < 0) { // If its non-numeric scale. // Dont use the scale item if the aggregation method is sum as adding items from a custom scale makes no sense. if ($firstrating->settings->aggregationmethod != RATING_AGGREGATE_SUM) { $scalerecord = $DB->get_record('scale', array('id' => -$firstrating->settings->scale->id)); if ($scalerecord) { $scalearray = explode(',', $scalerecord->scale); $aggregatetoreturn = $scalearray[$aggregatetoreturn - 1]; } } } $result->aggregate = $aggregatetoreturn; $result->count = $firstrating->count; $result->itemid = $itemid; } return $result; } /** * Get ratings created since a given time. * * @param stdClass $context context object * @param string $component component name * @param int $since the time to check * @return array list of ratings db records since the given timelimit * @since Moodle 3.2 */ public function get_component_ratings_since($context, $component, $since) { global $DB, $USER; $ratingssince = array(); $where = 'contextid = ? AND component = ? AND (timecreated > ? OR timemodified > ?)'; $ratings = $DB->get_records_select('rating', $where, array($context->id, $component, $since, $since)); // Check area by area if we have permissions. $permissions = array(); $rm = new rating_manager(); foreach ($ratings as $rating) { // Check if the permission array for the area is cached. if (!isset($permissions[$rating->ratingarea])) { $permissions[$rating->ratingarea] = $rm->get_plugin_permissions_array($context->id, $component, $rating->ratingarea); } if (($permissions[$rating->ratingarea]['view'] and $rating->userid == $USER->id) or ($permissions[$rating->ratingarea]['viewany'] or $permissions[$rating->ratingarea]['viewall'])) { $ratingssince[$rating->id] = $rating; } } return $ratingssince; } } // End rating_manager class definition. /** * The rating_exception class for exceptions specific to the ratings system * * @package core_rating * @category rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class rating_exception extends moodle_exception { /** * @var string The message to accompany the thrown exception */ public $message; /** * Generate exceptions that can be easily identified as coming from the ratings system * * @param string $errorcode the error code to generate */ public function __construct($errorcode) { $this->errorcode = $errorcode; $this->message = get_string($errorcode, 'error'); } } home/harasnat/www/learning/admin/lib.php 0000604 00000010647 15062104576 0014301 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains functions used by the admin pages * * @since Moodle 2.1 * @package admin * @copyright 2011 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function admin_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( 'admin-*' => get_string('page-admin-x', 'pagetype'), $pagetype => get_string('page-admin-current', 'pagetype') ); return $array; } /** * File serving. * * @param stdClass $course The course object. * @param stdClass $cm The cm object. * @param context $context The context object. * @param string $filearea The file area. * @param array $args List of arguments. * @param bool $forcedownload Whether or not to force the download of the file. * @param array $options Array of options. * @return void|false */ function core_admin_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { global $CFG; if (in_array($filearea, ['logo', 'logocompact', 'favicon'])) { $size = array_shift($args); // The path hides the size. $itemid = clean_param(array_shift($args), PARAM_INT); $filename = clean_param(array_shift($args), PARAM_FILE); $themerev = theme_get_revision(); if ($themerev <= 0) { // Normalise to 0 as -1 doesn't place well with paths. $themerev = 0; } // Extract the requested width and height. $maxwidth = 0; $maxheight = 0; if (preg_match('/^\d+x\d+$/', $size)) { list($maxwidth, $maxheight) = explode('x', $size); $maxwidth = clean_param($maxwidth, PARAM_INT); $maxheight = clean_param($maxheight, PARAM_INT); } $lifetime = 0; if ($itemid > 0 && $themerev == $itemid) { // The itemid is $CFG->themerev, when 0 or less no caching. Also no caching when they don't match. $lifetime = DAYSECS * 60; } // Anyone, including guests and non-logged in users, can view the logos. $options = ['cacheability' => 'public']; // Check if we've got a cached file to return. When lifetime is 0 then we don't want to cached one. $candidate = $CFG->localcachedir . "/core_admin/$themerev/$filearea/{$maxwidth}x{$maxheight}/$filename"; if (file_exists($candidate) && $lifetime > 0) { send_file($candidate, $filename, $lifetime, 0, false, false, '', false, $options); } // Find the original file. $fs = get_file_storage(); $filepath = "/{$context->id}/core_admin/{$filearea}/0/{$filename}"; if (!$file = $fs->get_file_by_hash(sha1($filepath))) { send_file_not_found(); } // Check whether width/height are specified, and we can resize the image (some types such as ICO cannot be resized). if (($maxwidth === 0 && $maxheight === 0) || !$filedata = $file->resize_image($maxwidth, $maxheight)) { if ($lifetime) { file_safe_save_content($file->get_content(), $candidate); } send_stored_file($file, $lifetime, 0, false, $options); } // If we don't want to cached the file, serve now and quit. if (!$lifetime) { send_content_uncached($filedata, $filename); } // Save, serve and quit. file_safe_save_content($filedata, $candidate); send_file($candidate, $filename, $lifetime, 0, false, false, '', false, $options); } send_file_not_found(); } home/harasnat/www/learning/competency/lib.php 0000604 00000026202 15062107573 0015351 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Competency lib. * * @package core_competency * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use core_competency\api; use core_competency\plan; use core_competency\url; use core_competency\user_competency; use core_competency\user_evidence; /** * Hook when a comment is added. * * @param stdClass $comment The comment. * @param stdClass $params The parameters. * @return array */ function core_competency_comment_add($comment, $params) { global $USER, $PAGE; if (!get_config('core_competency', 'enabled')) { return; } if ($params->commentarea == 'user_competency') { $uc = new user_competency($params->itemid); // Message both the user and the reviewer, except when they are the author of the message. $recipients = array($uc->get('userid')); if ($uc->get('reviewerid')) { $recipients[] = $uc->get('reviewerid'); } $recipients = array_diff($recipients, array($comment->userid)); if (empty($recipients)) { return; } // Get the sender. $user = $USER; if ($USER->id != $comment->userid) { $user = core_user::get_user($comment->userid); } $fullname = fullname($user); // Get the competency. $competency = $uc->get_competency(); $competencyname = format_string($competency->get('shortname'), true, array('context' => $competency->get_context())); // We want to send a message for one plan, trying to find an active one first, or the last modified one. $plan = null; $plans = $uc->get_plans(); foreach ($plans as $candidate) { if ($candidate->get('status') == plan::STATUS_ACTIVE) { $plan = $candidate; break; } else if (!empty($plan) && $plan->get('timemodified') < $candidate->get('timemodified')) { $plan = $candidate; } else if (empty($plan)) { $plan = $candidate; } } // Urls. // TODO MDL-52749 Replace the link to the plan with the user competency page. if (empty($plan)) { $urlname = get_string('userplans', 'core_competency'); $url = url::plans($uc->get('userid')); } else { $urlname = $competencyname; $url = url::user_competency_in_plan($uc->get('userid'), $uc->get('competencyid'), $plan->get('id')); } // Construct the message content. $fullmessagehtml = get_string('usercommentedonacompetencyhtml', 'core_competency', array( 'fullname' => $fullname, 'competency' => $competencyname, 'comment' => format_text($comment->content, $comment->format, array('context' => $params->context->id)), 'url' => $url->out(true), 'urlname' => $urlname, )); if ($comment->format == FORMAT_PLAIN || $comment->format == FORMAT_MOODLE) { $format = FORMAT_MOODLE; $fullmessage = get_string('usercommentedonacompetency', 'core_competency', array( 'fullname' => $fullname, 'competency' => $competencyname, 'comment' => $comment->content, 'url' => $url->out(false), )); } else { $format = FORMAT_HTML; $fullmessage = $fullmessagehtml; } $message = new \core\message\message(); $message->courseid = SITEID; $message->component = 'moodle'; $message->name = 'competencyusercompcomment'; $message->notification = 1; $message->userfrom = core_user::get_noreply_user(); $message->subject = get_string('usercommentedonacompetencysubject', 'core_competency', $fullname); $message->fullmessage = $fullmessage; $message->fullmessageformat = $format; $message->fullmessagehtml = $fullmessagehtml; $message->smallmessage = get_string('usercommentedonacompetencysmall', 'core_competency', array( 'fullname' => $fullname, 'competency' => $competencyname, )); $message->contexturl = $url->out(false); $message->contexturlname = $urlname; $userpicture = new \user_picture($user); $userpicture->size = 1; // Use f1 size. // Message each recipient. foreach ($recipients as $recipient) { $msgcopy = clone($message); $msgcopy->userto = $recipient; // Generate an out-of-session token for the user receiving the message. $userpicture->includetoken = $recipient; $msgcopy->customdata = [ 'notificationiconurl' => $userpicture->get_url($PAGE)->out(false), ]; message_send($msgcopy); } } else if ($params->commentarea == 'plan') { $plan = new plan($params->itemid); // Message both the user and the reviewer, except when they are the author of the message. $recipients = array($plan->get('userid')); if ($plan->get('reviewerid')) { $recipients[] = $plan->get('reviewerid'); } $recipients = array_diff($recipients, array($comment->userid)); if (empty($recipients)) { return; } // Get the sender. $user = $USER; if ($USER->id != $comment->userid) { $user = core_user::get_user($comment->userid); } $fullname = fullname($user); $planname = format_string($plan->get('name'), true, array('context' => $plan->get_context())); $urlname = $planname; $url = url::plan($plan->get('id')); // Construct the message content. $fullmessagehtml = get_string('usercommentedonaplanhtml', 'core_competency', array( 'fullname' => $fullname, 'plan' => $planname, 'comment' => format_text($comment->content, $comment->format, array('context' => $params->context->id)), 'url' => $url->out(true), 'urlname' => $urlname, )); if ($comment->format == FORMAT_PLAIN || $comment->format == FORMAT_MOODLE) { $format = FORMAT_MOODLE; $fullmessage = get_string('usercommentedonaplan', 'core_competency', array( 'fullname' => $fullname, 'plan' => $planname, 'comment' => $comment->content, 'url' => $url->out(false), )); } else { $format = FORMAT_HTML; $fullmessage = $fullmessagehtml; } $message = new \core\message\message(); $message->courseid = SITEID; $message->component = 'moodle'; $message->name = 'competencyplancomment'; $message->notification = 1; $message->userfrom = core_user::get_noreply_user(); $message->subject = get_string('usercommentedonaplansubject', 'core_competency', $fullname); $message->fullmessage = $fullmessage; $message->fullmessageformat = $format; $message->fullmessagehtml = $fullmessagehtml; $message->smallmessage = get_string('usercommentedonaplansmall', 'core_competency', array( 'fullname' => $fullname, 'plan' => $planname, )); $message->contexturl = $url->out(false); $message->contexturlname = $urlname; $userpicture = new \user_picture($user); $userpicture->size = 1; // Use f1 size. // Message each recipient. foreach ($recipients as $recipient) { $msgcopy = clone($message); $msgcopy->userto = $recipient; // Generate an out-of-session token for the user receiving the message. $userpicture->includetoken = $recipient; $msgcopy->customdata = [ 'notificationiconurl' => $userpicture->get_url($PAGE)->out(false), ]; message_send($msgcopy); } } } /** * Return the permissions of for the comments. * * @param stdClass $params The parameters. * @return array */ function core_competency_comment_permissions($params) { if (!get_config('core_competency', 'enabled')) { return array('post' => false, 'view' => false); } if ($params->commentarea == 'user_competency') { $uc = new user_competency($params->itemid); if ($uc->can_read()) { return array('post' => $uc->can_comment(), 'view' => $uc->can_read_comments()); } } else if ($params->commentarea == 'plan') { $plan = new plan($params->itemid); if ($plan->can_read()) { return array('post' => $plan->can_comment(), 'view' => $plan->can_read_comments()); } } return array('post' => false, 'view' => false); } /** * Validates comments. * * @param stdClass $params The parameters. * @return bool */ function core_competency_comment_validate($params) { if (!get_config('core_competency', 'enabled')) { return false; } if ($params->commentarea == 'user_competency') { if (!user_competency::record_exists($params->itemid)) { return false; } return true; } else if ($params->commentarea == 'plan') { if (!plan::record_exists($params->itemid)) { return false; } return true; } return false; } /** * File serving. * * @param stdClass $course The course object. * @param stdClass $cm The cm object. * @param context $context The context object. * @param string $filearea The file area. * @param array $args List of arguments. * @param bool $forcedownload Whether or not to force the download of the file. * @param array $options Array of options. * @return void|false */ function core_competency_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { global $CFG; if (!get_config('core_competency', 'enabled')) { return false; } $fs = get_file_storage(); $file = null; $itemid = array_shift($args); $filename = array_shift($args); $filepath = $args ? '/' .implode('/', $args) . '/' : '/'; if ($filearea == 'userevidence' && $context->contextlevel == CONTEXT_USER) { if (user_evidence::can_read_user($context->instanceid)) { $file = $fs->get_file($context->id, 'core_competency', $filearea, $itemid, $filepath, $filename); $forcedownload = true; } } if (!$file) { return false; } send_stored_file($file, null, 0, $forcedownload); } home/harasnat/www/learning/cohort/lib.php 0000604 00000063033 15062110001 0014460 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Cohort related management functions, this file needs to be included manually. * * @package core_cohort * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); define('COHORT_ALL', 0); define('COHORT_COUNT_MEMBERS', 1); define('COHORT_COUNT_ENROLLED_MEMBERS', 3); define('COHORT_WITH_MEMBERS_ONLY', 5); define('COHORT_WITH_ENROLLED_MEMBERS_ONLY', 17); define('COHORT_WITH_NOTENROLLED_MEMBERS_ONLY', 23); /** * Add new cohort. * * @param stdClass $cohort * @return int new cohort id */ function cohort_add_cohort($cohort) { global $DB, $CFG; if (!isset($cohort->name)) { throw new coding_exception('Missing cohort name in cohort_add_cohort().'); } if (!isset($cohort->idnumber)) { $cohort->idnumber = NULL; } if (!isset($cohort->description)) { $cohort->description = ''; } if (!isset($cohort->descriptionformat)) { $cohort->descriptionformat = FORMAT_HTML; } if (!isset($cohort->visible)) { $cohort->visible = 1; } if (empty($cohort->component)) { $cohort->component = ''; } if (empty($CFG->allowcohortthemes) && isset($cohort->theme)) { unset($cohort->theme); } if (empty($cohort->theme) || empty($CFG->allowcohortthemes)) { $cohort->theme = ''; } if (!isset($cohort->timecreated)) { $cohort->timecreated = time(); } if (!isset($cohort->timemodified)) { $cohort->timemodified = $cohort->timecreated; } $cohort->id = $DB->insert_record('cohort', $cohort); $handler = core_cohort\customfield\cohort_handler::create(); $handler->instance_form_save($cohort, true); $event = \core\event\cohort_created::create(array( 'context' => context::instance_by_id($cohort->contextid), 'objectid' => $cohort->id, )); $event->add_record_snapshot('cohort', $cohort); $event->trigger(); return $cohort->id; } /** * Update existing cohort. * @param stdClass $cohort * @return void */ function cohort_update_cohort($cohort) { global $DB, $CFG; if (property_exists($cohort, 'component') and empty($cohort->component)) { // prevent NULLs $cohort->component = ''; } // Only unset the cohort theme if allowcohortthemes is enabled to prevent the value from being overwritten. if (empty($CFG->allowcohortthemes) && isset($cohort->theme)) { unset($cohort->theme); } $cohort->timemodified = time(); // Update custom fields if there are any of them in the form. $handler = core_cohort\customfield\cohort_handler::create(); $handler->instance_form_save($cohort); $DB->update_record('cohort', $cohort); $event = \core\event\cohort_updated::create(array( 'context' => context::instance_by_id($cohort->contextid), 'objectid' => $cohort->id, )); $event->trigger(); } /** * Delete cohort. * @param stdClass $cohort * @return void */ function cohort_delete_cohort($cohort) { global $DB; if ($cohort->component) { // TODO: add component delete callback } $handler = core_cohort\customfield\cohort_handler::create(); $handler->delete_instance($cohort->id); $DB->delete_records('cohort_members', array('cohortid'=>$cohort->id)); $DB->delete_records('cohort', array('id'=>$cohort->id)); // Notify the competency subsystem. \core_competency\api::hook_cohort_deleted($cohort); $event = \core\event\cohort_deleted::create(array( 'context' => context::instance_by_id($cohort->contextid), 'objectid' => $cohort->id, )); $event->add_record_snapshot('cohort', $cohort); $event->trigger(); } /** * Somehow deal with cohorts when deleting course category, * we can not just delete them because they might be used in enrol * plugins or referenced in external systems. * @param stdClass|core_course_category $category * @return void */ function cohort_delete_category($category) { global $DB; // TODO: make sure that cohorts are really, really not used anywhere and delete, for now just move to parent or system context $oldcontext = context_coursecat::instance($category->id); if ($category->parent and $parent = $DB->get_record('course_categories', array('id'=>$category->parent))) { $parentcontext = context_coursecat::instance($parent->id); $sql = "UPDATE {cohort} SET contextid = :newcontext WHERE contextid = :oldcontext"; $params = array('oldcontext'=>$oldcontext->id, 'newcontext'=>$parentcontext->id); } else { $syscontext = context_system::instance(); $sql = "UPDATE {cohort} SET contextid = :newcontext WHERE contextid = :oldcontext"; $params = array('oldcontext'=>$oldcontext->id, 'newcontext'=>$syscontext->id); } $DB->execute($sql, $params); } /** * Add cohort member * @param int $cohortid * @param int $userid * @return void */ function cohort_add_member($cohortid, $userid) { global $DB; if ($DB->record_exists('cohort_members', array('cohortid'=>$cohortid, 'userid'=>$userid))) { // No duplicates! return; } $record = new stdClass(); $record->cohortid = $cohortid; $record->userid = $userid; $record->timeadded = time(); $DB->insert_record('cohort_members', $record); $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST); $event = \core\event\cohort_member_added::create(array( 'context' => context::instance_by_id($cohort->contextid), 'objectid' => $cohortid, 'relateduserid' => $userid, )); $event->add_record_snapshot('cohort', $cohort); $event->trigger(); } /** * Remove cohort member * @param int $cohortid * @param int $userid * @return void */ function cohort_remove_member($cohortid, $userid) { global $DB; $DB->delete_records('cohort_members', array('cohortid'=>$cohortid, 'userid'=>$userid)); $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST); $event = \core\event\cohort_member_removed::create(array( 'context' => context::instance_by_id($cohort->contextid), 'objectid' => $cohortid, 'relateduserid' => $userid, )); $event->add_record_snapshot('cohort', $cohort); $event->trigger(); } /** * Is this user a cohort member? * @param int $cohortid * @param int $userid * @return bool */ function cohort_is_member($cohortid, $userid) { global $DB; return $DB->record_exists('cohort_members', array('cohortid'=>$cohortid, 'userid'=>$userid)); } /** * Returns the list of cohorts visible to the current user in the given course. * * The following fields are returned in each record: id, name, contextid, idnumber, visible * Fields memberscnt and enrolledcnt will be also returned if requested * * @param context $currentcontext * @param int $withmembers one of the COHORT_XXX constants that allows to return non empty cohorts only * or cohorts with enroled/not enroled users, or just return members count * @param int $offset * @param int $limit * @param string $search * @param bool $withcustomfields if set to yes, then cohort custom fields will be included in the results. * @return array */ function cohort_get_available_cohorts($currentcontext, $withmembers = 0, $offset = 0, $limit = 25, $search = '', $withcustomfields = false) { global $DB; $params = array(); // Build context subquery. Find the list of parent context where user is able to see any or visible-only cohorts. // Since this method is normally called for the current course all parent contexts are already preloaded. $contextsany = array_filter($currentcontext->get_parent_context_ids(), function($a) { return has_capability("moodle/cohort:view", context::instance_by_id($a)); }); $contextsvisible = array_diff($currentcontext->get_parent_context_ids(), $contextsany); if (empty($contextsany) && empty($contextsvisible)) { // User does not have any permissions to view cohorts. return array(); } $subqueries = array(); if (!empty($contextsany)) { list($parentsql, $params1) = $DB->get_in_or_equal($contextsany, SQL_PARAMS_NAMED, 'ctxa'); $subqueries[] = 'c.contextid ' . $parentsql; $params = array_merge($params, $params1); } if (!empty($contextsvisible)) { list($parentsql, $params1) = $DB->get_in_or_equal($contextsvisible, SQL_PARAMS_NAMED, 'ctxv'); $subqueries[] = '(c.visible = 1 AND c.contextid ' . $parentsql. ')'; $params = array_merge($params, $params1); } $wheresql = '(' . implode(' OR ', $subqueries) . ')'; // Build the rest of the query. $fromsql = ""; $fieldssql = 'c.id, c.name, c.contextid, c.idnumber, c.visible'; $groupbysql = ''; $havingsql = ''; if ($withmembers) { $fieldssql .= ', s.memberscnt'; $subfields = "c.id, COUNT(DISTINCT cm.userid) AS memberscnt"; $groupbysql = " GROUP BY c.id"; $fromsql = " LEFT JOIN {cohort_members} cm ON cm.cohortid = c.id "; if (in_array($withmembers, array(COHORT_COUNT_ENROLLED_MEMBERS, COHORT_WITH_ENROLLED_MEMBERS_ONLY, COHORT_WITH_NOTENROLLED_MEMBERS_ONLY))) { list($esql, $params2) = get_enrolled_sql($currentcontext); $fromsql .= " LEFT JOIN ($esql) u ON u.id = cm.userid "; $params = array_merge($params2, $params); $fieldssql .= ', s.enrolledcnt'; $subfields .= ', COUNT(DISTINCT u.id) AS enrolledcnt'; } if ($withmembers == COHORT_WITH_MEMBERS_ONLY) { $havingsql = " HAVING COUNT(DISTINCT cm.userid) > 0"; } else if ($withmembers == COHORT_WITH_ENROLLED_MEMBERS_ONLY) { $havingsql = " HAVING COUNT(DISTINCT u.id) > 0"; } else if ($withmembers == COHORT_WITH_NOTENROLLED_MEMBERS_ONLY) { $havingsql = " HAVING COUNT(DISTINCT cm.userid) > COUNT(DISTINCT u.id)"; } } if ($search) { list($searchsql, $searchparams) = cohort_get_search_query($search); $wheresql .= ' AND ' . $searchsql; $params = array_merge($params, $searchparams); } if ($withmembers) { $sql = "SELECT " . str_replace('c.', 'cohort.', $fieldssql) . " FROM {cohort} cohort JOIN (SELECT $subfields FROM {cohort} c $fromsql WHERE $wheresql $groupbysql $havingsql ) s ON cohort.id = s.id ORDER BY cohort.name, cohort.idnumber"; } else { $sql = "SELECT $fieldssql FROM {cohort} c $fromsql WHERE $wheresql ORDER BY c.name, c.idnumber"; } $cohorts = $DB->get_records_sql($sql, $params, $offset, $limit); if ($withcustomfields) { $cohortids = array_keys($cohorts); $customfieldsdata = cohort_get_custom_fields_data($cohortids); foreach ($cohorts as $cohort) { $cohort->customfields = !empty($customfieldsdata[$cohort->id]) ? $customfieldsdata[$cohort->id] : []; } } return $cohorts; } /** * Check if cohort exists and user is allowed to access it from the given context. * * @param stdClass|int $cohortorid cohort object or id * @param context $currentcontext current context (course) where visibility is checked * @return boolean */ function cohort_can_view_cohort($cohortorid, $currentcontext) { global $DB; if (is_numeric($cohortorid)) { $cohort = $DB->get_record('cohort', array('id' => $cohortorid), 'id, contextid, visible'); } else { $cohort = $cohortorid; } if ($cohort && in_array($cohort->contextid, $currentcontext->get_parent_context_ids())) { if ($cohort->visible) { return true; } $cohortcontext = context::instance_by_id($cohort->contextid); if (has_capability('moodle/cohort:view', $cohortcontext)) { return true; } } return false; } /** * Get a cohort by id. Also does a visibility check and returns false if the user cannot see this cohort. * * @param stdClass|int $cohortorid cohort object or id * @param context $currentcontext current context (course) where visibility is checked * @param bool $withcustomfields if set to yes, then cohort custom fields will be included in the results. * @return stdClass|boolean */ function cohort_get_cohort($cohortorid, $currentcontext, $withcustomfields = false) { global $DB; if (is_numeric($cohortorid)) { $cohort = $DB->get_record('cohort', array('id' => $cohortorid), 'id, contextid, visible'); } else { $cohort = $cohortorid; } if ($cohort && in_array($cohort->contextid, $currentcontext->get_parent_context_ids())) { if (!$cohort->visible) { $cohortcontext = context::instance_by_id($cohort->contextid); if (!has_capability('moodle/cohort:view', $cohortcontext)) { return false; } } } else { return false; } if ($cohort && $withcustomfields) { $customfieldsdata = cohort_get_custom_fields_data([$cohort->id]); $cohort->customfields = !empty($customfieldsdata[$cohort->id]) ? $customfieldsdata[$cohort->id] : []; } return $cohort; } /** * Produces a part of SQL query to filter cohorts by the search string * * Called from {@link cohort_get_cohorts()}, {@link cohort_get_all_cohorts()} and {@link cohort_get_available_cohorts()} * * @access private * * @param string $search search string * @param string $tablealias alias of cohort table in the SQL query (highly recommended if other tables are used in query) * @return array of two elements - SQL condition and array of named parameters */ function cohort_get_search_query($search, $tablealias = '') { global $DB; $params = array(); if (empty($search)) { // This function should not be called if there is no search string, just in case return dummy query. return array('1=1', $params); } if ($tablealias && substr($tablealias, -1) !== '.') { $tablealias .= '.'; } $searchparam = '%' . $DB->sql_like_escape($search) . '%'; $conditions = array(); $fields = array('name', 'idnumber', 'description'); $cnt = 0; foreach ($fields as $field) { $conditions[] = $DB->sql_like($tablealias . $field, ':csearch' . $cnt, false); $params['csearch' . $cnt] = $searchparam; $cnt++; } $sql = '(' . implode(' OR ', $conditions) . ')'; return array($sql, $params); } /** * Get all the cohorts defined in given context. * * The function does not check user capability to view/manage cohorts in the given context * assuming that it has been already verified. * * @param int $contextid * @param int $page number of the current page * @param int $perpage items per page * @param string $search search string * @param bool $withcustomfields if set to yes, then cohort custom fields will be included in the results. * @return array Array(totalcohorts => int, cohorts => array, allcohorts => int) */ function cohort_get_cohorts($contextid, $page = 0, $perpage = 25, $search = '', $withcustomfields = false) { global $DB; $fields = "SELECT *"; $countfields = "SELECT COUNT(1)"; $sql = " FROM {cohort} WHERE contextid = :contextid"; $params = array('contextid' => $contextid); $order = " ORDER BY name ASC, idnumber ASC"; if (!empty($search)) { list($searchcondition, $searchparams) = cohort_get_search_query($search); $sql .= ' AND ' . $searchcondition; $params = array_merge($params, $searchparams); } $totalcohorts = $allcohorts = $DB->count_records('cohort', array('contextid' => $contextid)); if (!empty($search)) { $totalcohorts = $DB->count_records_sql($countfields . $sql, $params); } $cohorts = $DB->get_records_sql($fields . $sql . $order, $params, $page*$perpage, $perpage); if ($withcustomfields) { $cohortids = array_keys($cohorts); $customfieldsdata = cohort_get_custom_fields_data($cohortids); foreach ($cohorts as $cohort) { $cohort->customfields = !empty($customfieldsdata[$cohort->id]) ? $customfieldsdata[$cohort->id] : []; } } return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts, 'allcohorts' => $allcohorts); } /** * Get all the cohorts defined anywhere in system. * * The function assumes that user capability to view/manage cohorts on system level * has already been verified. This function only checks if such capabilities have been * revoked in child (categories) contexts. * * @param int $page number of the current page * @param int $perpage items per page * @param string $search search string * @param bool $withcustomfields if set to yes, then cohort custom fields will be included in the results. * @return array Array(totalcohorts => int, cohorts => array, allcohorts => int) */ function cohort_get_all_cohorts($page = 0, $perpage = 25, $search = '', $withcustomfields = false) { global $DB; $fields = "SELECT c.*, ".context_helper::get_preload_record_columns_sql('ctx'); $countfields = "SELECT COUNT(*)"; $sql = " FROM {cohort} c JOIN {context} ctx ON ctx.id = c.contextid "; $params = array(); $wheresql = ''; if ($excludedcontexts = cohort_get_invisible_contexts()) { list($excludedsql, $excludedparams) = $DB->get_in_or_equal($excludedcontexts, SQL_PARAMS_NAMED, 'excl', false); $wheresql = ' WHERE c.contextid '.$excludedsql; $params = array_merge($params, $excludedparams); } $totalcohorts = $allcohorts = $DB->count_records_sql($countfields . $sql . $wheresql, $params); if (!empty($search)) { list($searchcondition, $searchparams) = cohort_get_search_query($search, 'c'); $wheresql .= ($wheresql ? ' AND ' : ' WHERE ') . $searchcondition; $params = array_merge($params, $searchparams); $totalcohorts = $DB->count_records_sql($countfields . $sql . $wheresql, $params); } $order = " ORDER BY c.name ASC, c.idnumber ASC"; $cohorts = $DB->get_records_sql($fields . $sql . $wheresql . $order, $params, $page*$perpage, $perpage); if ($withcustomfields) { $cohortids = array_keys($cohorts); $customfieldsdata = cohort_get_custom_fields_data($cohortids); } foreach ($cohorts as $cohort) { // Preload used contexts, they will be used to check view/manage/assign capabilities and display categories names. context_helper::preload_from_record($cohort); if ($withcustomfields) { $cohort->customfields = !empty($customfieldsdata[$cohort->id]) ? $customfieldsdata[$cohort->id] : []; } } return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts, 'allcohorts' => $allcohorts); } /** * Get all the cohorts where the given user is member of. * * @param int $userid * @param bool $withcustomfields if set to yes, then cohort custom fields will be included in the results. * @return array Array */ function cohort_get_user_cohorts($userid, $withcustomfields = false) { global $DB; $sql = 'SELECT c.* FROM {cohort} c JOIN {cohort_members} cm ON c.id = cm.cohortid WHERE cm.userid = ? AND c.visible = 1'; $cohorts = $DB->get_records_sql($sql, array($userid)); if ($withcustomfields) { $cohortids = array_keys($cohorts); $customfieldsdata = cohort_get_custom_fields_data($cohortids); foreach ($cohorts as $cohort) { $cohort->customfields = !empty($customfieldsdata[$cohort->id]) ? $customfieldsdata[$cohort->id] : []; } } return $cohorts; } /** * Get the user cohort theme. * * If the user is member of one cohort, will return this cohort theme (if defined). * If the user is member of 2 or more cohorts, will return the theme if all them have the same * theme (null themes are ignored). * * @param int $userid * @return string|null */ function cohort_get_user_cohort_theme($userid) { $cohorts = cohort_get_user_cohorts($userid); $theme = null; foreach ($cohorts as $cohort) { if (!empty($cohort->theme)) { if (null === $theme) { $theme = $cohort->theme; } else if ($theme != $cohort->theme) { return null; } } } return $theme; } /** * Returns list of contexts where cohorts are present but current user does not have capability to view/manage them. * * This function is called from {@link cohort_get_all_cohorts()} to ensure correct pagination in rare cases when user * is revoked capability in child contexts. It assumes that user's capability to view/manage cohorts on system * level has already been verified. * * @access private * * @return array array of context ids */ function cohort_get_invisible_contexts() { global $DB; if (is_siteadmin()) { // Shortcut, admin can do anything and can not be prohibited from any context. return array(); } $records = $DB->get_recordset_sql("SELECT DISTINCT ctx.id, ".context_helper::get_preload_record_columns_sql('ctx')." ". "FROM {context} ctx JOIN {cohort} c ON ctx.id = c.contextid "); $excludedcontexts = array(); foreach ($records as $ctx) { context_helper::preload_from_record($ctx); if (context::instance_by_id($ctx->id) == context_system::instance()) { continue; // System context cohorts should be available and permissions already checked. } if (!has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:view'), context::instance_by_id($ctx->id))) { $excludedcontexts[] = $ctx->id; } } $records->close(); return $excludedcontexts; } /** * Returns navigation controls (tabtree) to be displayed on cohort management pages * * @param context $context system or category context where cohorts controls are about to be displayed * @param moodle_url $currenturl * @return null|renderable */ function cohort_edit_controls(context $context, moodle_url $currenturl) { $tabs = array(); $currenttab = 'view'; $viewurl = new moodle_url('/cohort/index.php', array('contextid' => $context->id)); if (($searchquery = $currenturl->get_param('search'))) { $viewurl->param('search', $searchquery); } if ($context->contextlevel == CONTEXT_SYSTEM) { $tabs[] = new tabobject('view', new moodle_url($viewurl, array('showall' => 0)), get_string('systemcohorts', 'cohort')); $tabs[] = new tabobject('viewall', new moodle_url($viewurl, array('showall' => 1)), get_string('allcohorts', 'cohort')); if ($currenturl->get_param('showall')) { $currenttab = 'viewall'; } } else { $tabs[] = new tabobject('view', $viewurl, get_string('cohorts', 'cohort')); } if (has_capability('moodle/cohort:manage', $context)) { $addurl = new moodle_url('/cohort/edit.php', array('contextid' => $context->id)); $tabs[] = new tabobject('addcohort', $addurl, get_string('addcohort', 'cohort')); if ($currenturl->get_path() === $addurl->get_path() && !$currenturl->param('id')) { $currenttab = 'addcohort'; } $uploadurl = new moodle_url('/cohort/upload.php', array('contextid' => $context->id)); $tabs[] = new tabobject('uploadcohorts', $uploadurl, get_string('uploadcohorts', 'cohort')); if ($currenturl->get_path() === $uploadurl->get_path()) { $currenttab = 'uploadcohorts'; } } if (count($tabs) > 1) { return new tabtree($tabs, $currenttab); } return null; } /** * Implements callback inplace_editable() allowing to edit values in-place * * @param string $itemtype * @param int $itemid * @param mixed $newvalue * @return \core\output\inplace_editable */ function core_cohort_inplace_editable($itemtype, $itemid, $newvalue) { if ($itemtype === 'cohortname') { return \core_cohort\output\cohortname::update($itemid, $newvalue); } else if ($itemtype === 'cohortidnumber') { return \core_cohort\output\cohortidnumber::update($itemid, $newvalue); } } /** * Returns a list of valid themes which can be displayed in a selector. * * @return array as (string)themename => (string)get_string_theme */ function cohort_get_list_of_themes() { $themes = array(); $allthemes = get_list_of_themes(); foreach ($allthemes as $key => $theme) { if (empty($theme->hidefromselector)) { $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); } } return $themes; } /** * Returns custom fields data for provided cohorts. * * @param array $cohortids a list of cohort IDs to provide data for. * @return \core_customfield\data_controller[] */ function cohort_get_custom_fields_data(array $cohortids): array { $result = []; if (!empty($cohortids)) { $handler = core_cohort\customfield\cohort_handler::create(); $result = $handler->get_instances_data($cohortids, true); } return $result; } home/harasnat/www/learning/course/lib.php 0000604 00000615477 15062110002 0014502 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Library of useful functions * * @copyright 1999 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_course */ defined('MOODLE_INTERNAL') || die; use core_course\external\course_summary_exporter; use core_courseformat\base as course_format; use core\output\local\action_menu\subpanel as action_menu_subpanel; require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/filelib.php'); require_once($CFG->libdir.'/datalib.php'); require_once($CFG->dirroot.'/course/format/lib.php'); define('COURSE_MAX_LOGS_PER_PAGE', 1000); // Records. define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds. /** * Number of courses to display when summaries are included. * @var int * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead. */ define('COURSE_MAX_SUMMARIES_PER_PAGE', 10); // Max courses in log dropdown before switching to optional. define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000); // Max users in log dropdown before switching to optional. define('COURSE_MAX_USERS_PER_DROPDOWN', 1000); define('FRONTPAGENEWS', '0'); define('FRONTPAGECATEGORYNAMES', '2'); define('FRONTPAGECATEGORYCOMBO', '4'); define('FRONTPAGEENROLLEDCOURSELIST', '5'); define('FRONTPAGEALLCOURSELIST', '6'); define('FRONTPAGECOURSESEARCH', '7'); // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage. define('EXCELROWS', 65535); define('FIRSTUSEDEXCELROW', 3); define('MOD_CLASS_ACTIVITY', 0); define('MOD_CLASS_RESOURCE', 1); define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden'); define('COURSE_TIMELINE_ALL', 'all'); define('COURSE_TIMELINE_PAST', 'past'); define('COURSE_TIMELINE_INPROGRESS', 'inprogress'); define('COURSE_TIMELINE_FUTURE', 'future'); define('COURSE_TIMELINE_SEARCH', 'search'); define('COURSE_FAVOURITES', 'favourites'); define('COURSE_TIMELINE_HIDDEN', 'hidden'); define('COURSE_CUSTOMFIELD', 'customfield'); define('COURSE_DB_QUERY_LIMIT', 1000); /** Searching for all courses that have no value for the specified custom field. */ define('COURSE_CUSTOMFIELD_EMPTY', -1); // Course activity chooser footer default display option. define('COURSE_CHOOSER_FOOTER_NONE', 'hidden'); // Download course content options. define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0); define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1); define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2); function make_log_url($module, $url) { switch ($module) { case 'course': if (strpos($url, 'report/') === 0) { // there is only one report type, course reports are deprecated $url = "/$url"; break; } case 'file': case 'login': case 'lib': case 'admin': case 'category': case 'mnet course': if (strpos($url, '../') === 0) { $url = ltrim($url, '.'); } else { $url = "/course/$url"; } break; case 'calendar': $url = "/calendar/$url"; break; case 'user': case 'blog': $url = "/$module/$url"; break; case 'upload': $url = $url; break; case 'coursetags': $url = '/'.$url; break; case 'library': case '': $url = '/'; break; case 'message': $url = "/message/$url"; break; case 'notes': $url = "/notes/$url"; break; case 'tag': $url = "/tag/$url"; break; case 'role': $url = '/'.$url; break; case 'grade': $url = "/grade/$url"; break; default: $url = "/mod/$module/$url"; break; } //now let's sanitise urls - there might be some ugly nasties:-( $parts = explode('?', $url); $script = array_shift($parts); if (strpos($script, 'http') === 0) { $script = clean_param($script, PARAM_URL); } else { $script = clean_param($script, PARAM_PATH); } $query = ''; if ($parts) { $query = implode('', $parts); $query = str_replace('&', '&', $query); // both & and & are stored in db :-| $parts = explode('&', $query); $eq = urlencode('='); foreach ($parts as $key=>$part) { $part = urlencode(urldecode($part)); $part = str_replace($eq, '=', $part); $parts[$key] = $part; } $query = '?'.implode('&', $parts); } return $script.$query; } function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='', $modname="", $modid=0, $modaction="", $groupid=0) { global $CFG, $DB; // It is assumed that $date is the GMT time of midnight for that day, // and so the next 86400 seconds worth of logs are printed. /// Setup for group handling. // TODO: I don't understand group/context/etc. enough to be able to do // something interesting with it here // What is the context of a remote course? /// If the group mode is separate, and this user does not have editing privileges, /// then only the user's group can be viewed. //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) { // $groupid = get_current_group($course->id); //} /// If this course doesn't have groups, no groupid can be specified. //else if (!$course->groupmode) { // $groupid = 0; //} $groupid = 0; $joins = array(); $where = ''; $qry = "SELECT l.*, u.firstname, u.lastname, u.picture FROM {mnet_log} l LEFT JOIN {user} u ON l.userid = u.id WHERE "; $params = array(); $where .= "l.hostid = :hostid"; $params['hostid'] = $hostid; // TODO: Is 1 really a magic number referring to the sitename? if ($course != SITEID || $modid != 0) { $where .= " AND l.course=:courseid"; $params['courseid'] = $course; } if ($modname) { $where .= " AND l.module = :modname"; $params['modname'] = $modname; } if ('site_errors' === $modid) { $where .= " AND ( l.action='error' OR l.action='infected' )"; } else if ($modid) { //TODO: This assumes that modids are the same across sites... probably //not true $where .= " AND l.cmid = :modid"; $params['modid'] = $modid; } if ($modaction) { $firstletter = substr($modaction, 0, 1); if ($firstletter == '-') { $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true); $params['modaction'] = '%'.substr($modaction, 1).'%'; } else { $where .= " AND ".$DB->sql_like('l.action', ':modaction', false); $params['modaction'] = '%'.$modaction.'%'; } } if ($user) { $where .= " AND l.userid = :user"; $params['user'] = $user; } if ($date) { $enddate = $date + 86400; $where .= " AND l.time > :date AND l.time < :enddate"; $params['date'] = $date; $params['enddate'] = $enddate; } $result = array(); $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params); if(!empty($result['totalcount'])) { $where .= " ORDER BY $order"; $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum); } else { $result['logs'] = array(); } return $result; } /** * Checks the integrity of the course data. * * In summary - compares course_sections.sequence and course_modules.section. * * More detailed, checks that: * - course_sections.sequence contains each module id not more than once in the course * - for each moduleid from course_sections.sequence the field course_modules.section * refers to the same section id (this means course_sections.sequence is more * important if they are different) * - ($fullcheck only) each module in the course is present in one of * course_sections.sequence * - ($fullcheck only) removes non-existing course modules from section sequences * * If there are any mismatches, the changes are made and records are updated in DB. * * Course cache is NOT rebuilt if there are any errors! * * This function is used each time when course cache is being rebuilt with $fullcheck = false * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true * * @param int $courseid id of the course * @param array $rawmods result of funciton {@link get_course_mods()} - containst * the list of enabled course modules in the course. Retrieved from DB if not specified. * Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway. * @param array $sections records from course_sections table for this course. * Retrieved from DB if not specified * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing * course modules from sequences. Only to be used in site maintenance mode when we are * sure that another user is not in the middle of the process of moving/removing a module. * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages. * @return array array of messages with found problems. Empty output means everything is ok */ function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) { global $DB; $messages = array(); if ($sections === null) { $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence'); } if ($fullcheck) { // Retrieve all records from course_modules regardless of module type visibility. $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section'); } if ($rawmods === null) { $rawmods = get_course_mods($courseid); } if (!$fullcheck && (empty($sections) || empty($rawmods))) { // If either of the arrays is empty, no modules are displayed anyway. return true; } $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. '; // First make sure that each module id appears in section sequences only once. // If it appears in several section sequences the last section wins. // If it appears twice in one section sequence, the first occurence wins. $modsection = array(); foreach ($sections as $sectionid => $section) { $sections[$sectionid]->newsequence = $section->sequence; if (!empty($section->sequence)) { $sequence = explode(",", $section->sequence); $sequenceunique = array_unique($sequence); if (count($sequenceunique) != count($sequence)) { // Some course module id appears in this section sequence more than once. ksort($sequenceunique); // Preserve initial order of modules. $sequence = array_values($sequenceunique); $sections[$sectionid]->newsequence = join(',', $sequence); $messages[] = $debuggingprefix.'Sequence for course section ['. $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"'; } foreach ($sequence as $cmid) { if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) { // Some course module id appears to be in more than one section's sequences. $wrongsectionid = $modsection[$cmid]; $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ','); $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['. $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']'; } $modsection[$cmid] = $sectionid; } } } // Add orphaned modules to their sections if they exist or to section 0 otherwise. if ($fullcheck) { foreach ($rawmods as $cmid => $mod) { if (!isset($modsection[$cmid])) { // This is a module that is not mentioned in course_section.sequence at all. // Add it to the section $mod->section or to the last available section. if ($mod->section && isset($sections[$mod->section])) { $modsection[$cmid] = $mod->section; } else { $firstsection = reset($sections); $modsection[$cmid] = $firstsection->id; } $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ','); $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['. $modsection[$cmid].']'; } } foreach ($modsection as $cmid => $sectionid) { if (!isset($rawmods[$cmid])) { // Section $sectionid refers to module id that does not exist. $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ','); $messages[] = $debuggingprefix.'Course module ['.$cmid. '] does not exist but is present in the sequence of section ['.$sectionid.']'; } } } // Update changed sections. if (!$checkonly && !empty($messages)) { foreach ($sections as $sectionid => $section) { if ($section->newsequence !== $section->sequence) { $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence)); } } } // Now make sure that all modules point to the correct sections. foreach ($rawmods as $cmid => $mod) { if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) { if (!$checkonly) { $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid])); } $messages[] = $debuggingprefix.'Course module ['.$cmid. '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']'; } } return $messages; } /** * Returns an array where the key is the module name (component name without 'mod_') * and the value is a lang_string object with a human-readable string. * * @param bool $plural If true, the function returns the plural forms of the names. * @param bool $resetcache If true, the static cache will be reset * @return lang_string[] Localised human-readable names of all used modules. */ function get_module_types_names($plural = false, $resetcache = false) { static $modnames = null; global $DB, $CFG; if ($modnames === null || $resetcache) { $modnames = array(0 => array(), 1 => array()); if ($allmods = $DB->get_records("modules")) { foreach ($allmods as $mod) { if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) { $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true); $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true); } } } } return $modnames[(int)$plural]; } /** * Set highlighted section. Only one section can be highlighted at the time. * * @param int $courseid course id * @param int $marker highlight section with this number, 0 means remove higlightin * @return void */ function course_set_marker($courseid, $marker) { global $DB, $COURSE; $DB->set_field("course", "marker", $marker, array('id' => $courseid)); if ($COURSE && $COURSE->id == $courseid) { $COURSE->marker = $marker; } core_courseformat\base::reset_course_cache($courseid); course_modinfo::clear_instance_cache($courseid); } /** * For a given course section, marks it visible or hidden, * and does the same for every activity in that section * * @param int $courseid course id * @param int $sectionnumber The section number to adjust * @param int $visibility The new visibility * @return array A list of resources which were hidden in the section */ function set_section_visible($courseid, $sectionnumber, $visibility) { global $DB; $resourcestotoggle = array(); if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) { course_update_section($courseid, $section, array('visible' => $visibility)); // Determine which modules are visible for AJAX update $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array(); if (!empty($modules)) { list($insql, $params) = $DB->get_in_or_equal($modules); $select = 'id ' . $insql . ' AND visible = ?'; array_push($params, $visibility); if (!$visibility) { $select .= ' AND visibleold = 1'; } $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params); } } return $resourcestotoggle; } /** * Return the course category context for the category with id $categoryid, except * that if $categoryid is 0, return the system context. * * @param integer $categoryid a category id or 0. * @return context the corresponding context */ function get_category_or_system_context($categoryid) { if ($categoryid) { return context_coursecat::instance($categoryid, IGNORE_MISSING); } else { return context_system::instance(); } } /** * Print the buttons relating to course requests. * * @param context $context current page context. * @deprecated since Moodle 4.0 * @todo Final deprecation MDL-73976 */ function print_course_request_buttons($context) { global $CFG, $DB, $OUTPUT; debugging("print_course_request_buttons() is deprecated. " . "This is replaced with the category_action_bar tertiary navigation.", DEBUG_DEVELOPER); if (empty($CFG->enablecourserequests)) { return; } if (course_request::can_request($context)) { // Print a button to request a new course. $params = []; if ($context instanceof context_coursecat) { $params['category'] = $context->instanceid; } echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params), get_string('requestcourse'), 'get'); } /// Print a button to manage pending requests if (has_capability('moodle/site:approvecourse', $context)) { $disabled = !$DB->record_exists('course_request', array()); echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled)); } } /** * Does the user have permission to edit things in this category? * * @param integer $categoryid The id of the category we are showing, or 0 for system context. * @return boolean has_any_capability(array(...), ...); in the appropriate context. */ function can_edit_in_category($categoryid = 0) { $context = get_category_or_system_context($categoryid); return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context); } /// MODULE FUNCTIONS ///////////////////////////////////////////////////////////////// function add_course_module($mod) { global $DB; $mod->added = time(); unset($mod->id); $cmid = $DB->insert_record("course_modules", $mod); rebuild_course_cache($mod->course, true); return $cmid; } /** * Creates a course section and adds it to the specified position * * @param int|stdClass $courseorid course id or course object * @param int $position position to add to, 0 means to the end. If position is greater than * number of existing secitons, the section is added to the end. This will become sectionnum of the * new section. All existing sections at this or bigger position will be shifted down. * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist * @return stdClass created section object */ function course_create_section($courseorid, $position = 0, $skipcheck = false) { global $DB; $courseid = is_object($courseorid) ? $courseorid->id : $courseorid; // Find the last sectionnum among existing sections. if ($skipcheck) { $lastsection = $position - 1; } else { $lastsection = (int)$DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', [$courseid]); } // First add section to the end. $cw = new stdClass(); $cw->course = $courseid; $cw->section = $lastsection + 1; $cw->summary = ''; $cw->summaryformat = FORMAT_HTML; $cw->sequence = ''; $cw->name = null; $cw->visible = 1; $cw->availability = null; $cw->timemodified = time(); $cw->id = $DB->insert_record("course_sections", $cw); // Now move it to the specified position. if ($position > 0 && $position <= $lastsection) { $course = is_object($courseorid) ? $courseorid : get_course($courseorid); move_section_to($course, $cw->section, $position, true); $cw->section = $position; } core\event\course_section_created::create_from_section($cw)->trigger(); rebuild_course_cache($courseid, true); return $cw; } /** * Creates missing course section(s) and rebuilds course cache * * @param int|stdClass $courseorid course id or course object * @param int|array $sections list of relative section numbers to create * @return bool if there were any sections created */ function course_create_sections_if_missing($courseorid, $sections) { if (!is_array($sections)) { $sections = array($sections); } $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all()); if ($newsections = array_diff($sections, $existing)) { foreach ($newsections as $sectionnum) { course_create_section($courseorid, $sectionnum, true); } return true; } return false; } /** * Adds an existing module to the section * * Updates both tables {course_sections} and {course_modules} * * Note: This function does not use modinfo PROVIDED that the section you are * adding the module to already exists. If the section does not exist, it will * build modinfo if necessary and create the section. * * @param int|stdClass $courseorid course id or course object * @param int $cmid id of the module already existing in course_modules table * @param int $sectionnum relative number of the section (field course_sections.section) * If section does not exist it will be created * @param int|stdClass $beforemod id or object with field id corresponding to the module * before which the module needs to be included. Null for inserting in the * end of the section * @return int The course_sections ID where the module is inserted */ function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) { global $DB, $COURSE; if (is_object($beforemod)) { $beforemod = $beforemod->id; } if (is_object($courseorid)) { $courseid = $courseorid->id; } else { $courseid = $courseorid; } // Do not try to use modinfo here, there is no guarantee it is valid! $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING); if (!$section) { // This function call requires modinfo. course_create_sections_if_missing($courseorid, $sectionnum); $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST); } $modarray = explode(",", trim($section->sequence)); if (empty($section->sequence)) { $newsequence = "$cmid"; } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) { $insertarray = array($cmid, $beforemod); array_splice($modarray, $key[0], 1, $insertarray); $newsequence = implode(",", $modarray); } else { $newsequence = "$section->sequence,$cmid"; } $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id)); $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid)); rebuild_course_cache($courseid, true); return $section->id; // Return course_sections ID that was used. } /** * Change the group mode of a course module. * * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}. * * @param int $id course module ID. * @param int $groupmode the new groupmode value. * @return bool True if the $groupmode was updated. */ function set_coursemodule_groupmode($id, $groupmode) { global $DB; $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST); if ($cm->groupmode != $groupmode) { $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id)); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } return ($cm->groupmode != $groupmode); } function set_coursemodule_idnumber($id, $idnumber) { global $DB; $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST); if ($cm->idnumber != $idnumber) { $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id)); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } return ($cm->idnumber != $idnumber); } /** * Set downloadcontent value to course module. * * @param int $id The id of the module. * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled. * @return bool True if downloadcontent has been updated, false otherwise. */ function set_downloadcontent(int $id, bool $downloadcontent): bool { global $DB; $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST); if ($cm->downloadcontent != $downloadcontent) { $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]); rebuild_course_cache($cm->course, true); } return ($cm->downloadcontent != $downloadcontent); } /** * Set the visibility of a module and inherent properties. * * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}. * * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered * has been moved to {@link set_section_visible()} which was the only place from which * the parameter was used. * * @param int $id of the module * @param int $visible state of the module * @param int $visibleoncoursepage state of the module on the course page * @return bool false when the module was not found, true otherwise */ function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) { global $DB, $CFG; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) { return false; } // Create events and propagate visibility to associated grade items if the value has changed. // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades. if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) { return true; } if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) { return false; } if (($cm->visible != $visible) && ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) { foreach($events as $event) { if ($visible) { $event = new calendar_event($event); $event->toggle_visibility(true); } else { $event = new calendar_event($event); $event->toggle_visibility(false); } } } // Updating visible and visibleold to keep them in sync. Only changing a section visibility will // affect visibleold to allow for an original visibility restore. See set_section_visible(). $cminfo = new stdClass(); $cminfo->id = $id; $cminfo->visible = $visible; $cminfo->visibleoncoursepage = $visibleoncoursepage; $cminfo->visibleold = $visible; $DB->update_record('course_modules', $cminfo); // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there. // Note that this must be done after updating the row in course_modules, in case // the modules grade_item_update function needs to access $cm->visible. if ($cm->visible != $visible && plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) && component_callback_exists('mod_' . $modulename, 'grade_item_update')) { $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST); component_callback('mod_' . $modulename, 'grade_item_update', array($instance)); } else if ($cm->visible != $visible) { $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course)); if ($grade_items) { foreach ($grade_items as $grade_item) { $grade_item->set_hidden(!$visible); } } } \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); return true; } /** * Changes the course module name * * @param int $id course module id * @param string $name new value for a name * @return bool whether a change was made */ function set_coursemodule_name($id, $name) { global $CFG, $DB; require_once($CFG->libdir . '/gradelib.php'); $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST); $module = new \stdClass(); $module->id = $cm->instance; // Escape strings as they would be by mform. if (!empty($CFG->formatstringstriptags)) { $module->name = clean_param($name, PARAM_TEXT); } else { $module->name = clean_param($name, PARAM_CLEANHTML); } if ($module->name === $cm->name || strval($module->name) === '') { return false; } if (\core_text::strlen($module->name) > 255) { throw new \moodle_exception('maximumchars', 'moodle', '', 255); } $module->timemodified = time(); $DB->update_record($cm->modname, $module); $cm->name = $module->name; \core\event\course_module_updated::create_from_cm($cm)->trigger(); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); // Attempt to update the grade item if relevant. $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance)); $grademodule->cmidnumber = $cm->idnumber; $grademodule->modname = $cm->modname; grade_update_mod_grades($grademodule); // Update calendar events with the new name. course_module_update_calendar_events($cm->modname, $grademodule, $cm); return true; } /** * This function will handle the whole deletion process of a module. This includes calling * the modules delete_instance function, deleting files, events, grades, conditional data, * the data in the course_module and course_sections table and adding a module deletion * event to the DB. * * @param int $cmid the course module id * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook. * @throws moodle_exception * @since Moodle 2.5 */ function course_delete_module($cmid, $async = false) { // Check the 'course_module_background_deletion_recommended' hook first. // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested. // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it. // It's up to plugins to handle things like whether or not they are enabled. if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { if ($pluginfunction()) { return course_module_flag_for_async_deletion($cmid); } } } } global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->dirroot.'/blog/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); // Get the course module. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { return true; } // Get the module context. $modcontext = context_module::instance($cm->id); // Get the course module name. $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); // Get the file location of the delete_instance function for this module. $modlib = "$CFG->dirroot/mod/$modulename/lib.php"; // Include the file required to call the delete_instance function for this module. if (file_exists($modlib)) { require_once($modlib); } else { throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null, "Cannot delete this module as the file mod/$modulename/lib.php is missing."); } $deleteinstancefunction = $modulename . '_delete_instance'; // Ensure the delete_instance function exists for this module. if (!function_exists($deleteinstancefunction)) { throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null, "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); } // Allow plugins to use this course module before we completely delete it. if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($cm); } } } // Call the delete_instance function, if it returns false throw an exception. if (!$deleteinstancefunction($cm->instance)) { throw new moodle_exception('cannotdeletemoduleinstance', '', '', null, "Cannot delete the module $modulename (instance)."); } question_delete_activity($cm); // Remove all module files in case modules forget to do that. $fs = get_file_storage(); $fs->delete_area_files($modcontext->id); // Delete events from calendar. if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) { $coursecontext = context_course::instance($cm->course); foreach($events as $event) { $event->context = $coursecontext; $calendarevent = calendar_event::load($event); $calendarevent->delete(); } } // Delete grade items, outcome items and grades attached to modules. if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename, 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) { foreach ($grade_items as $grade_item) { $grade_item->delete('moddelete'); } } // Delete associated blogs and blog tag instances. blog_remove_associations_for_module($modcontext->id); // Delete completion and availability data; it is better to do this even if the // features are not turned on, in case they were turned on previously (these will be // very quick on an empty table). $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id)); $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]); $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id, 'course' => $cm->course, 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY)); // Delete all tag instances associated with the instance of this module. core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id); core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); // Notify the competency subsystem. \core_competency\api::hook_course_module_deleted($cm); // Delete the context. context_helper::delete_instance(CONTEXT_MODULE, $cm->id); // Delete the module from the course_modules table. $DB->delete_records('course_modules', array('id' => $cm->id)); // Delete module from that section. if (!delete_mod_from_section($cm->id, $cm->section)) { throw new moodle_exception('cannotdeletemodulefromsection', '', '', null, "Cannot delete the module $modulename (instance) from section."); } // Trigger event for course module delete action. $event = \core\event\course_module_deleted::create(array( 'courseid' => $cm->course, 'context' => $modcontext, 'objectid' => $cm->id, 'other' => array( 'modulename' => $modulename, 'instanceid' => $cm->instance, ) )); $event->add_record_snapshot('course_modules', $cm); $event->trigger(); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); rebuild_course_cache($cm->course, false, true); } /** * Schedule a course module for deletion in the background using an adhoc task. * * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion. * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'. * * @param int $cmid the course module id. * @return bool whether the module was successfully scheduled for deletion. * @throws \moodle_exception */ function course_module_flag_for_async_deletion($cmid) { global $CFG, $DB, $USER; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->dirroot.'/blog/lib.php'); require_once($CFG->dirroot.'/calendar/lib.php'); // Get the course module. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { return true; } // We need to be reasonably certain the deletion is going to succeed before we background the process. // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required. // Get the course module name. $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); // Get the file location of the delete_instance function for this module. $modlib = "$CFG->dirroot/mod/$modulename/lib.php"; // Include the file required to call the delete_instance function for this module. if (file_exists($modlib)) { require_once($modlib); } else { throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null, "Cannot delete this module as the file mod/$modulename/lib.php is missing."); } $deleteinstancefunction = $modulename . '_delete_instance'; // Ensure the delete_instance function exists for this module. if (!function_exists($deleteinstancefunction)) { throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null, "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); } // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for. $cm->deletioninprogress = '1'; $DB->update_record('course_modules', $cm); // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal. $removaltask = new \core_course\task\course_delete_modules(); $removaltask->set_custom_data(array( 'cms' => array($cm), 'userid' => $USER->id, 'realuserid' => \core\session\manager::get_realuser()->id )); // Queue the task for the next run. \core\task\manager::queue_adhoc_task($removaltask); // Reset the course cache to hide the module. rebuild_course_cache($cm->course, true); } /** * Checks whether the given course has any course modules scheduled for adhoc deletion. * * @param int $courseid the id of the course. * @param bool $onlygradable whether to check only gradable modules or all modules. * @return bool true if the course contains any modules pending deletion, false otherwise. */ function course_modules_pending_deletion(int $courseid, bool $onlygradable = false) : bool { if (empty($courseid)) { return false; } if ($onlygradable) { // Fetch modules with grade items. if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) { // Return early when there is none. return false; } } $modinfo = get_fast_modinfo($courseid); foreach ($modinfo->get_cms() as $module) { if ($module->deletioninprogress == '1') { if ($onlygradable) { // Check if the module being deleted is in the list of course modules with grade items. foreach ($coursegradeitems as $coursegradeitem) { if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) { // The module being deleted is within the gradable modules. return true; } } } else { return true; } } } return false; } /** * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course. * * @param int $courseid the course id. * @param string $modulename the module name. E.g. 'assign', 'book', etc. * @param int $instanceid the module instance id. * @return bool true if the course module is pending deletion, false otherwise. */ function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) { if (empty($courseid) || empty($modulename) || empty($instanceid)) { return false; } $modinfo = get_fast_modinfo($courseid); $instances = $modinfo->get_instances_of($modulename); return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress; } function delete_mod_from_section($modid, $sectionid) { global $DB; if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) { $modarray = explode(",", $section->sequence); if ($key = array_keys ($modarray, $modid)) { array_splice($modarray, $key[0], 1); $newsequence = implode(",", $modarray); $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id)); rebuild_course_cache($section->course, true); return true; } else { return false; } } return false; } /** * This function updates the calendar events from the information stored in the module table and the course * module table. * * @param string $modulename Module name * @param stdClass $instance Module object. Either the $instance or the $cm must be supplied. * @param stdClass $cm Course module object. Either the $instance or the $cm must be supplied. * @return bool Returns true if calendar events are updated. * @since Moodle 3.3.4 */ function course_module_update_calendar_events($modulename, $instance = null, $cm = null) { global $DB; if (isset($instance) || isset($cm)) { if (!isset($instance)) { $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST); } if (!isset($cm)) { $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course); } if (!empty($cm)) { course_module_calendar_event_update_process($instance, $cm); } return true; } return false; } /** * Update all instances through out the site or in a course. * * @param string $modulename Module type to update. * @param integer $courseid Course id to update events. 0 for the whole site. * @return bool Returns True if the update was successful. * @since Moodle 3.3.4 */ function course_module_bulk_update_calendar_events($modulename, $courseid = 0) { global $DB; $instances = null; if ($courseid) { if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) { return false; } } else { if (!$instances = $DB->get_records($modulename)) { return false; } } foreach ($instances as $instance) { if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) { course_module_calendar_event_update_process($instance, $cm); } } return true; } /** * Calendar events for a module instance are updated. * * @param stdClass $instance Module instance object. * @param stdClass $cm Course Module object. * @since Moodle 3.3.4 */ function course_module_calendar_event_update_process($instance, $cm) { // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which // will remove the completion events. $refresheventsfunction = $cm->modname . '_refresh_events'; if (function_exists($refresheventsfunction)) { call_user_func($refresheventsfunction, $cm->course, $instance, $cm); } $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null; \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected); } /** * Moves a section within a course, from a position to another. * Be very careful: $section and $destination refer to section number, * not id!. * * @param object $course * @param int $section Section number (not id!!!) * @param int $destination * @param bool $ignorenumsections * @return boolean Result */ function move_section_to($course, $section, $destination, $ignorenumsections = false) { /// Moves a whole course section up and down within the course global $USER, $DB; if (!$destination && $destination != 0) { return true; } // compartibility with course formats using field 'numsections' $courseformatoptions = course_get_format($course)->get_format_options(); if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) && ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) { return false; } // Get all sections for this course and re-order them (2 of them should now share the same section number) if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id), 'section ASC, id ASC', 'id, section')) { return false; } $movedsections = reorder_sections($sections, $section, $destination); // Update all sections. Do this in 2 steps to avoid breaking database // uniqueness constraint $transaction = $DB->start_delegated_transaction(); foreach ($movedsections as $id => $position) { if ((int) $sections[$id] !== $position) { $DB->set_field('course_sections', 'section', -$position, ['id' => $id]); // Invalidate the section cache by given section id. course_modinfo::purge_course_section_cache_by_id($course->id, $id); } } foreach ($movedsections as $id => $position) { if ((int) $sections[$id] !== $position) { $DB->set_field('course_sections', 'section', $position, ['id' => $id]); // Invalidate the section cache by given section id. course_modinfo::purge_course_section_cache_by_id($course->id, $id); } } // If we move the highlighted section itself, then just highlight the destination. // Adjust the higlighted section location if we move something over it either direction. if ($section == $course->marker) { course_set_marker($course->id, $destination); } else if ($section > $course->marker && $course->marker >= $destination) { course_set_marker($course->id, $course->marker+1); } else if ($section < $course->marker && $course->marker <= $destination) { course_set_marker($course->id, $course->marker-1); } $transaction->allow_commit(); rebuild_course_cache($course->id, true, true); return true; } /** * This method will delete a course section and may delete all modules inside it. * * No permissions are checked here, use {@link course_can_delete_section()} to * check if section can actually be deleted. * * @param int|stdClass $course * @param int|stdClass|section_info $section * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it. * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook. * @return bool whether section was deleted */ function course_delete_section($course, $section, $forcedeleteifnotempty = true, $async = false) { global $DB; // Prepare variables. $courseid = (is_object($course)) ? $course->id : (int)$course; $sectionnum = (is_object($section)) ? $section->section : (int)$section; $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum)); if (!$section) { // No section exists, can't proceed. return false; } // Check the 'course_module_background_deletion_recommended' hook first. // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested. // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it. // It's up to plugins to handle things like whether or not they are enabled. if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { if ($pluginfunction()) { return course_delete_section_async($section, $forcedeleteifnotempty); } } } } $format = course_get_format($course); $sectionname = $format->get_section_name($section); // Delete section. $result = $format->delete_section($section, $forcedeleteifnotempty); // Trigger an event for course section deletion. if ($result) { $context = context_course::instance($courseid); $event = \core\event\course_section_deleted::create( array( 'objectid' => $section->id, 'courseid' => $courseid, 'context' => $context, 'other' => array( 'sectionnum' => $section->section, 'sectionname' => $sectionname, ) ) ); $event->add_record_snapshot('course_sections', $section); $event->trigger(); } return $result; } /** * Course section deletion, using an adhoc task for deletion of the modules it contains. * 1. Schedule all modules within the section for adhoc removal. * 2. Move all modules to course section 0. * 3. Delete the resulting empty section. * * @param \stdClass $section the section to schedule for deletion. * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules. * @return bool true if the section was scheduled for deletion, false otherwise. */ function course_delete_section_async($section, $forcedeleteifnotempty = true) { global $DB, $USER; // Objects only, and only valid ones. if (!is_object($section) || empty($section->id)) { return false; } // Does the object currently exist in the DB for removal (check for stale objects). $section = $DB->get_record('course_sections', array('id' => $section->id)); if (!$section || !$section->section) { // No section exists, or the section is 0. Can't proceed. return false; } // Check whether the section can be removed. if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) { return false; } $format = course_get_format($section->course); $sectionname = $format->get_section_name($section); // Flag those modules having no existing deletion flag. Some modules may have been scheduled for deletion manually, and we don't // want to create additional adhoc deletion tasks for these. Moving them to section 0 will suffice. $affectedmods = $DB->get_records_select('course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?', [$section->course, $section->id, 1], '', 'id'); $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $section->course, 'section' => $section->id]); // Move all modules to section 0. $modules = $DB->get_records('course_modules', ['section' => $section->id], ''); $sectionzero = $DB->get_record('course_sections', ['course' => $section->course, 'section' => '0']); foreach ($modules as $mod) { moveto_module($mod, $sectionzero); } // Create and queue an adhoc task for the deletion of the modules. $removaltask = new \core_course\task\course_delete_modules(); $data = array( 'cms' => $affectedmods, 'userid' => $USER->id, 'realuserid' => \core\session\manager::get_realuser()->id ); $removaltask->set_custom_data($data); \core\task\manager::queue_adhoc_task($removaltask); // Delete the now empty section, passing in only the section number, which forces the function to fetch a new object. // The refresh is needed because the section->sequence is now stale. $result = $format->delete_section($section->section, $forcedeleteifnotempty); // Trigger an event for course section deletion. if ($result) { $context = \context_course::instance($section->course); $event = \core\event\course_section_deleted::create( array( 'objectid' => $section->id, 'courseid' => $section->course, 'context' => $context, 'other' => array( 'sectionnum' => $section->section, 'sectionname' => $sectionname, ) ) ); $event->add_record_snapshot('course_sections', $section); $event->trigger(); } rebuild_course_cache($section->course, true); return $result; } /** * Updates the course section * * This function does not check permissions or clean values - this has to be done prior to calling it. * * @param int|stdClass $course * @param stdClass $section record from course_sections table - it will be updated with the new values * @param array|stdClass $data */ function course_update_section($course, $section, $data) { global $DB; $courseid = (is_object($course)) ? $course->id : (int)$course; // Some fields can not be updated using this method. $data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence')); $changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible); if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) { throw new moodle_exception('maximumchars', 'moodle', '', 255); } // Update record in the DB and course format options. $data['id'] = $section->id; $data['timemodified'] = time(); $DB->update_record('course_sections', $data); // Invalidate the section cache by given section id. course_modinfo::purge_course_section_cache_by_id($courseid, $section->id); rebuild_course_cache($courseid, false, true); course_get_format($courseid)->update_section_format_options($data); // Update fields of the $section object. foreach ($data as $key => $value) { if (property_exists($section, $key)) { $section->$key = $value; } } // Trigger an event for course section update. $event = \core\event\course_section_updated::create( array( 'objectid' => $section->id, 'courseid' => $courseid, 'context' => context_course::instance($courseid), 'other' => array('sectionnum' => $section->section) ) ); $event->trigger(); // If section visibility was changed, hide the modules in this section too. if ($changevisibility && !empty($section->sequence)) { $modules = explode(',', $section->sequence); foreach ($modules as $moduleid) { if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) { if ($data['visible']) { // As we unhide the section, we use the previously saved visibility stored in visibleold. set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage); } else { // We hide the section, so we hide the module but we store the original state in visibleold. set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage); $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]); \course_modinfo::purge_course_module_cache($cm->course, $cm->id); } \core\event\course_module_updated::create_from_cm($cm)->trigger(); } } rebuild_course_cache($courseid, false, true); } } /** * Checks if the current user can delete a section (if course format allows it and user has proper permissions). * * @param int|stdClass $course * @param int|stdClass|section_info $section * @return bool */ function course_can_delete_section($course, $section) { if (is_object($section)) { $section = $section->section; } if (!$section) { // Not possible to delete 0-section. return false; } // Course format should allow to delete sections. if (!course_get_format($course)->can_delete_section($section)) { return false; } // Make sure user has capability to update course and move sections. $context = context_course::instance(is_object($course) ? $course->id : $course); if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) { return false; } // Make sure user has capability to delete each activity in this section. $modinfo = get_fast_modinfo($course); if (!empty($modinfo->sections[$section])) { foreach ($modinfo->sections[$section] as $cmid) { if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) { return false; } } } return true; } /** * Reordering algorithm for course sections. Given an array of section->section indexed by section->id, * an original position number and a target position number, rebuilds the array so that the * move is made without any duplication of section positions. * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to * insert a section before the first one, you must give 0 as the target (section 0 can never be moved). * * @param array $sections * @param int $origin_position * @param int $target_position * @return array */ function reorder_sections($sections, $origin_position, $target_position) { if (!is_array($sections)) { return false; } // We can't move section position 0 if ($origin_position < 1) { echo "We can't move section position 0"; return false; } // Locate origin section in sections array if (!$origin_key = array_search($origin_position, $sections)) { echo "searched position not in sections array"; return false; // searched position not in sections array } // Extract origin section $origin_section = $sections[$origin_key]; unset($sections[$origin_key]); // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!) $found = false; $append_array = array(); foreach ($sections as $id => $position) { if ($found) { $append_array[$id] = $position; unset($sections[$id]); } if ($position == $target_position) { if ($target_position < $origin_position) { $append_array[$id] = $position; unset($sections[$id]); } $found = true; } } // Append moved section $sections[$origin_key] = $origin_section; // Append rest of array (if applicable) if (!empty($append_array)) { foreach ($append_array as $id => $position) { $sections[$id] = $position; } } // Renumber positions $position = 0; foreach ($sections as $id => $p) { $sections[$id] = $position; $position++; } return $sections; } /** * Move the module object $mod to the specified $section * If $beforemod exists then that is the module * before which $modid should be inserted * * @param stdClass|cm_info $mod * @param stdClass|section_info $section * @param int|stdClass $beforemod id or object with field id corresponding to the module * before which the module needs to be included. Null for inserting in the * end of the section * @return int new value for module visibility (0 or 1) */ function moveto_module($mod, $section, $beforemod=NULL) { global $OUTPUT, $DB; // Current module visibility state - return value of this function. $modvisible = $mod->visible; // Remove original module from original section. if (! delete_mod_from_section($mod->id, $mod->section)) { echo $OUTPUT->notification("Could not delete module from existing section"); } // Add the module into the new section. course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod); // If moving to a hidden section then hide module. if ($mod->section != $section->id) { if (!$section->visible && $mod->visible) { // Module was visible but must become hidden after moving to hidden section. $modvisible = 0; set_coursemodule_visible($mod->id, 0); // Set visibleold to 1 so module will be visible when section is made visible. $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id)); } if ($section->visible && !$mod->visible) { // Hidden module was moved to the visible section, restore the module visibility from visibleold. set_coursemodule_visible($mod->id, $mod->visibleold); $modvisible = $mod->visibleold; } } return $modvisible; } /** * Returns the list of all editing actions that current user can perform on the module * * @param cm_info $mod The module to produce editing buttons for * @param int $indent The current indenting (default -1 means no move left-right actions) * @param int $sr The section to link back to (used for creating the links) * @return array array of action_link or pix_icon objects */ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { global $COURSE, $SITE, $CFG; static $str; $coursecontext = context_course::instance($mod->course); $modcontext = context_module::instance($mod->id); $courseformat = course_get_format($mod->get_course()); $usecomponents = $courseformat->supports_components(); $sectioninfo = $mod->get_section_info(); $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign'); $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'); // No permission to edit anything. if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) { return array(); } $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); if (!isset($str)) { $str = get_strings( [ 'delete', 'move', 'moveright', 'moveleft', 'editsettings', 'duplicate', 'availability' ], 'moodle' ); $str->assign = get_string('assignroles', 'role'); $str->groupmode = get_string('groupmode', 'group'); } $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); if ($sr !== null) { $baseurl->param('sr', $sr); } $actions = array(); // Update. if ($hasmanageactivities) { $actions['update'] = new action_menu_link_secondary( new moodle_url($baseurl, array('update' => $mod->id)), new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')), $str->editsettings, array('class' => 'editing_update', 'data-action' => 'update') ); } // Move (only for component compatible formats). if ($usecomponents) { $actions['move'] = new action_menu_link_secondary( new moodle_url($baseurl, [ 'sesskey' => sesskey(), 'copy' => $mod->id, ]), new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']), $str->move, [ 'class' => 'editing_movecm', 'data-action' => 'moveCm', 'data-id' => $mod->id, ] ); } // Indent. if ($hasmanageactivities && $indent >= 0) { $indentlimits = new stdClass(); $indentlimits->min = 0; // Legacy indentation could continue using a limit of 16, // but components based formats will be forced to use one level indentation only. $indentlimits->max = ($usecomponents) ? 1 : 16; if (right_to_left()) { // Exchange arrows on RTL $rightarrow = 't/left'; $leftarrow = 't/right'; } else { $rightarrow = 't/right'; $leftarrow = 't/left'; } if ($indent >= $indentlimits->max) { $enabledclass = 'hidden'; } else { $enabledclass = ''; } $actions['moveright'] = new action_menu_link_secondary( new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '1']), new pix_icon($rightarrow, '', 'moodle', ['class' => 'iconsmall']), $str->moveright, [ 'class' => 'editing_moveright ' . $enabledclass, 'data-action' => ($usecomponents) ? 'cmMoveRight' : 'moveright', 'data-keepopen' => true, 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); if ($indent <= $indentlimits->min) { $enabledclass = 'hidden'; } else { $enabledclass = ''; } $actions['moveleft'] = new action_menu_link_secondary( new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '-1']), new pix_icon($leftarrow, '', 'moodle', ['class' => 'iconsmall']), $str->moveleft, [ 'class' => 'editing_moveleft ' . $enabledclass, 'data-action' => ($usecomponents) ? 'cmMoveLeft' : 'moveleft', 'data-keepopen' => true, 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); } // Hide/Show/Available/Unavailable. if (has_capability('moodle/course:activityvisibility', $modcontext)) { $availabilityclass = $courseformat->get_output_classname('content\\cm\\visibility'); /** @var core_courseformat\output\local\content\cm\visibility */ $availability = new $availabilityclass($courseformat, $sectioninfo, $mod); $availabilitychoice = $availability->get_choice_list(); if ($availabilitychoice->count_options() > 1) { $actions['availability'] = new action_menu_subpanel( $str->availability, $availabilitychoice, ['class' => 'editing_availability'], new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')) ); } } // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php) if (has_all_capabilities($dupecaps, $coursecontext) && plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) && course_allowed_module($mod->get_course(), $mod->modname)) { $actions['duplicate'] = new action_menu_link_secondary( new moodle_url($baseurl, ['duplicate' => $mod->id]), new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')), $str->duplicate, [ 'class' => 'editing_duplicate', 'data-action' => ($courseformat->supports_components()) ? 'cmDuplicate' : 'duplicate', 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); } // Assign. if (has_capability('moodle/role:assign', $modcontext)){ $actions['assign'] = new action_menu_link_secondary( new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)), new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')), $str->assign, array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr) ); } // Groupmode. if ($courseformat->show_groupmode($mod) && $usecomponents) { $groupmodeclass = $courseformat->get_output_classname('content\\cm\\groupmode'); /** @var core_courseformat\output\local\content\cm\groupmode */ $groupmode = new $groupmodeclass($courseformat, $sectioninfo, $mod); $actions['groupmode'] = new action_menu_subpanel( $str->groupmode, $groupmode->get_choice_list(), ['class' => 'editing_groupmode'], new pix_icon('i/groupv', '', 'moodle', ['class' => 'iconsmall']) ); } // Delete. if ($hasmanageactivities) { $actions['delete'] = new action_menu_link_secondary( new moodle_url($baseurl, ['delete' => $mod->id]), new pix_icon('t/delete', '', 'moodle', ['class' => 'iconsmall']), $str->delete, [ 'class' => 'editing_delete text-danger', 'data-action' => ($usecomponents) ? 'cmDelete' : 'delete', 'data-sectionreturn' => $sr, 'data-id' => $mod->id, ] ); } return $actions; } /** * Returns the move action. * * @param cm_info $mod The module to produce a move button for * @param int $sr The section to link back to (used for creating the links) * @return The markup for the move action, or an empty string if not available. */ function course_get_cm_move(cm_info $mod, $sr = null) { global $OUTPUT; static $str; static $baseurl; $modcontext = context_module::instance($mod->id); $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); if (!isset($str)) { $str = get_strings(array('move')); } if (!isset($baseurl)) { $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); if ($sr !== null) { $baseurl->param('sr', $sr); } } if ($hasmanageactivities) { $pixicon = 'i/dragdrop'; if (!course_ajax_enabled($mod->get_course())) { // Override for course frontpage until we get drag/drop working there. $pixicon = 't/move'; } $attributes = [ 'class' => 'editing_move', 'data-action' => 'move', 'data-sectionreturn' => $sr, 'title' => $str->move, 'aria-label' => $str->move, ]; return html_writer::link( new moodle_url($baseurl, ['copy' => $mod->id]), $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']), $attributes ); } return ''; } /** * given a course object with shortname & fullname, this function will * truncate the the number of chars allowed and add ... if it was too long */ function course_format_name ($course,$max=100) { $context = context_course::instance($course->id); $shortname = format_string($course->shortname, true, array('context' => $context)); $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); $str = $shortname.': '. $fullname; if (core_text::strlen($str) <= $max) { return $str; } else { return core_text::substr($str,0,$max-3).'...'; } } /** * Is the user allowed to add this type of module to this course? * @param object $course the course settings. Only $course->id is used. * @param string $modname the module name. E.g. 'forum' or 'quiz'. * @param \stdClass $user the user to check, defaults to the global user if not provided. * @return bool whether the current user is allowed to add this type of module to this course. */ function course_allowed_module($course, $modname, \stdClass $user = null) { global $USER; $user = $user ?? $USER; if (is_numeric($modname)) { throw new coding_exception('Function course_allowed_module no longer supports numeric module ids. Please update your code to pass the module name.'); } $capability = 'mod/' . $modname . ':addinstance'; if (!get_capability_info($capability)) { // Debug warning that the capability does not exist, but no more than once per page. static $warned = array(); $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) { debugging('The module ' . $modname . ' does not define the standard capability ' . $capability , DEBUG_DEVELOPER); $warned[$modname] = 1; } // If the capability does not exist, the module can always be added. return true; } $coursecontext = context_course::instance($course->id); return has_capability($capability, $coursecontext, $user); } /** * Efficiently moves many courses around while maintaining * sortorder in order. * * @param array $courseids is an array of course ids * @param int $categoryid * @return bool success */ function move_courses($courseids, $categoryid) { global $DB; if (empty($courseids)) { // Nothing to do. return false; } if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) { return false; } $courseids = array_reverse($courseids); $newparent = context_coursecat::instance($category->id); $i = 1; list($where, $params) = $DB->get_in_or_equal($courseids); $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname'); foreach ($dbcourses as $dbcourse) { $course = new stdClass(); $course->id = $dbcourse->id; $course->timemodified = time(); $course->category = $category->id; $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++; if ($category->visible == 0) { // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get // to previous state if somebody unhides the category. $course->visible = 0; } $DB->update_record('course', $course); // Update context, so it can be passed to event. $context = context_course::instance($course->id); $context->update_moved($newparent); // Trigger a course updated event. $event = \core\event\course_updated::create(array( 'objectid' => $course->id, 'context' => context_course::instance($course->id), 'other' => array('shortname' => $dbcourse->shortname, 'fullname' => $dbcourse->fullname, 'updatedfields' => array('category' => $category->id)) )); $event->trigger(); } fix_course_sortorder(); cache_helper::purge_by_event('changesincourse'); return true; } /** * Returns the display name of the given section that the course prefers * * Implementation of this function is provided by course format * @see core_courseformat\base::get_section_name() * * @param int|stdClass $courseorid The course to get the section name for (object or just course id) * @param int|stdClass $section Section object from database or just field course_sections.section * @return string Display name that the course format prefers, e.g. "Week 2" */ function get_section_name($courseorid, $section) { return course_get_format($courseorid)->get_section_name($section); } /** * Tells if current course format uses sections * * @param string $format Course format ID e.g. 'weeks' $course->format * @return bool */ function course_format_uses_sections($format) { $course = new stdClass(); $course->format = $format; return course_get_format($course)->uses_sections(); } /** * Returns the information about the ajax support in the given source format * * The returned object's property (boolean)capable indicates that * the course format supports Moodle course ajax features. * * @param string $format * @return stdClass */ function course_format_ajax_support($format) { $course = new stdClass(); $course->format = $format; return course_get_format($course)->supports_ajax(); } /** * Can the current user delete this course? * Course creators have exception, * 1 day after the creation they can sill delete the course. * @param int $courseid * @return boolean */ function can_delete_course($courseid) { global $USER; $context = context_course::instance($courseid); if (has_capability('moodle/course:delete', $context)) { return true; } // hack: now try to find out if creator created this course recently (1 day) if (!has_capability('moodle/course:create', $context)) { return false; } $since = time() - 60*60*24; $course = get_course($courseid); if ($course->timecreated < $since) { return false; // Return if the course was not created in last 24 hours. } $logmanger = get_log_manager(); $readers = $logmanger->get_readers('\core\log\sql_reader'); $reader = reset($readers); if (empty($reader)) { return false; // No log reader found. } // A proper reader. $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since"; $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created'); return (bool)$reader->get_events_select_count($select, $params); } /** * Save the Your name for 'Some role' strings. * * @param integer $courseid the id of this course. * @param array $data the data that came from the course settings form. */ function save_local_role_names($courseid, $data) { global $DB; $context = context_course::instance($courseid); foreach ($data as $fieldname => $value) { if (strpos($fieldname, 'role_') !== 0) { continue; } list($ignored, $roleid) = explode('_', $fieldname); // make up our mind whether we want to delete, update or insert if (!$value) { $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid)); } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) { $rolename->name = $value; $DB->update_record('role_names', $rolename); } else { $rolename = new stdClass; $rolename->contextid = $context->id; $rolename->roleid = $roleid; $rolename->name = $value; $DB->insert_record('role_names', $rolename); } // This will ensure the course contacts cache is purged.. core_course_category::role_assignment_changed($roleid, $context); } } /** * Returns options to use in course overviewfiles filemanager * * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id; * may be empty if course does not exist yet (course create form) * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc. * or null if overviewfiles are disabled */ function course_overviewfiles_options($course) { global $CFG; if (empty($CFG->courseoverviewfileslimit)) { return null; } // Create accepted file types based on config value, falling back to default all. $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext); if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) { $acceptedtypes = '*'; } $options = array( 'maxfiles' => $CFG->courseoverviewfileslimit, 'maxbytes' => $CFG->maxbytes, 'subdirs' => 0, 'accepted_types' => $acceptedtypes ); if (!empty($course->id)) { $options['context'] = context_course::instance($course->id); } else if (is_int($course) && $course > 0) { $options['context'] = context_course::instance($course); } return $options; } /** * Create a course and either return a $course object * * Please note this functions does not verify any access control, * the calling code is responsible for all validation (usually it is the form definition). * * @param array $editoroptions course description editor options * @param object $data - all the data needed for an entry in the 'course' table * @return object new course instance */ function create_course($data, $editoroptions = NULL) { global $DB, $CFG; //check the categoryid - must be given for all new courses $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST); // Check if the shortname already exists. if (!empty($data->shortname)) { if ($DB->record_exists('course', array('shortname' => $data->shortname))) { throw new moodle_exception('shortnametaken', '', '', $data->shortname); } } // Check if the idnumber already exists. if (!empty($data->idnumber)) { if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) { throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber); } } if (empty($CFG->enablecourserelativedates)) { // Make sure we're not setting the relative dates mode when the setting is disabled. unset($data->relativedatesmode); } if ($errorcode = course_validate_dates((array)$data)) { throw new moodle_exception($errorcode); } // Check if timecreated is given. $data->timecreated = !empty($data->timecreated) ? $data->timecreated : time(); $data->timemodified = $data->timecreated; // place at beginning of any category $data->sortorder = 0; if ($editoroptions) { // summary text is updated later, we need context to store the files first $data->summary = ''; $data->summary_format = $data->summary_editor['format']; } // Get default completion settings as a fallback in case the enablecompletion field is not set. $courseconfig = get_config('moodlecourse'); $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED; $enablecompletion = $data->enablecompletion ?? $defaultcompletion; // Unset showcompletionconditions when completion tracking is not enabled for the course. if ($enablecompletion == COMPLETION_DISABLED) { unset($data->showcompletionconditions); } else if (!isset($data->showcompletionconditions)) { // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults. // This scenario can happen when a course is created through data generators or through a web service. $data->showcompletionconditions = $courseconfig->showcompletionconditions; } if (!isset($data->visible)) { // data not from form, add missing visibility info $data->visible = $category->visible; } $data->visibleold = $data->visible; $newcourseid = $DB->insert_record('course', $data); $context = context_course::instance($newcourseid, MUST_EXIST); if ($editoroptions) { // Save the files used in the summary editor and store $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid)); $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid)); } if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) { // Save the course overviewfiles $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); } // update course format options course_get_format($newcourseid)->update_course_format_options($data); $course = course_get_format($newcourseid)->get_course(); fix_course_sortorder(); // purge appropriate caches in case fix_course_sortorder() did not change anything cache_helper::purge_by_event('changesincourse'); // Trigger a course created event. $event = \core\event\course_created::create(array( 'objectid' => $course->id, 'context' => $context, 'other' => array('shortname' => $course->shortname, 'fullname' => $course->fullname) )); $event->trigger(); // Setup the blocks blocks_add_default_course_blocks($course); // Create default section and initial sections if specified (unless they've already been created earlier). // We do not want to call course_create_sections_if_missing() because to avoid creating course cache. $numsections = isset($data->numsections) ? $data->numsections : 0; $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]); $newsections = array_diff(range(0, $numsections), $existingsections); foreach ($newsections as $sectionnum) { course_create_section($newcourseid, $sectionnum, true); } // Save any custom role names. save_local_role_names($course->id, (array)$data); // set up enrolments enrol_course_updated(true, $course, $data); // Update course tags. if (isset($data->tags)) { core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $data->tags); } // Set up communication. if (core_communication\api::is_available()) { // Check for default provider config setting. $defaultprovider = get_config('moodlecourse', 'coursecommunicationprovider'); $provider = (isset($data->selectedcommunication)) ? $data->selectedcommunication : $defaultprovider; if (!empty($provider)) { // Prepare the communication api data. $courseimage = course_get_courseimage($course); $communicationroomname = !empty($data->communicationroomname) ? $data->communicationroomname : $data->fullname; // Communication api call. $communication = \core_communication\api::load_by_instance( context: $context, component: 'core_course', instancetype: 'coursecommunication', instanceid: $course->id, provider: $provider, ); $communication->create_and_configure_room( $communicationroomname, $courseimage ?: null, $data, ); } } // Save custom fields if there are any of them in the form. $handler = core_course\customfield\course_handler::create(); // Make sure to set the handler's parent context first. $coursecatcontext = context_coursecat::instance($category->id); $handler->set_parent_context($coursecatcontext); // Save the custom field data. $data->id = $course->id; $handler->instance_form_save($data, true); return $course; } /** * Update a course. * * Please note this functions does not verify any access control, * the calling code is responsible for all validation (usually it is the form definition). * * @param object $data - all the data needed for an entry in the 'course' table * @param array $editoroptions course description editor options * @return void */ function update_course($data, $editoroptions = NULL) { global $DB, $CFG; // Prevent changes on front page course. if ($data->id == SITEID) { throw new moodle_exception('invalidcourse', 'error'); } $oldcourse = course_get_format($data->id)->get_course(); $context = context_course::instance($oldcourse->id); // Make sure we're not changing whatever the course's relativedatesmode setting is. unset($data->relativedatesmode); // Capture the updated fields for the log data. $updatedfields = []; foreach (get_object_vars($oldcourse) as $field => $value) { if ($field == 'summary_editor') { if (($data->$field)['text'] !== $value['text']) { // The summary might be very long, we don't wan't to fill up the log record with the full text. $updatedfields[$field] = '(updated)'; } } else if ($field == 'tags' && isset($data->tags)) { // Tags might not have the same array keys, just check the values. if (array_values($data->$field) !== array_values($value)) { $updatedfields[$field] = $data->$field; } } else { if (isset($data->$field) && $data->$field != $value) { $updatedfields[$field] = $data->$field; } } } $data->timemodified = time(); if ($editoroptions) { $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); } if ($overviewfilesoptions = course_overviewfiles_options($data->id)) { $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); } // Check we don't have a duplicate shortname. if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) { if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) { throw new moodle_exception('shortnametaken', '', '', $data->shortname); } } // Check we don't have a duplicate idnumber. if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) { if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) { throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber); } } if ($errorcode = course_validate_dates((array)$data)) { throw new moodle_exception($errorcode); } if (!isset($data->category) or empty($data->category)) { // prevent nulls and 0 in category field unset($data->category); } $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category); if (!isset($data->visible)) { // data not from form, add missing visibility info $data->visible = $oldcourse->visible; } if ($data->visible != $oldcourse->visible) { // reset the visibleold flag when manually hiding/unhiding course $data->visibleold = $data->visible; $changesincoursecat = true; } else { if ($movecat) { $newcategory = $DB->get_record('course_categories', array('id'=>$data->category)); if (empty($newcategory->visible)) { // make sure when moving into hidden category the course is hidden automatically $data->visible = 0; } } } // Set newsitems to 0 if format does not support announcements. if (isset($data->format)) { $newcourseformat = course_get_format((object)['format' => $data->format]); if (!$newcourseformat->supports_news()) { $data->newsitems = 0; } } // Set showcompletionconditions to null when completion tracking has been disabled for the course. if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) { $data->showcompletionconditions = null; } // Check if provider is selected. $provider = $data->selectedcommunication ?? null; // If the course moved to hidden category, set provider to none. if ($changesincoursecat && empty($data->visible)) { $provider = 'none'; } // Attempt to get the communication provider if it wasn't provided in the data. if (empty($provider) && core_communication\api::is_available()) { $provider = \core_communication\api::load_by_instance( context: $context, component: 'core_course', instancetype: 'coursecommunication', instanceid: $data->id, )->get_provider(); } // Communication api call. if (!empty($provider) && core_communication\api::is_available()) { // Prepare the communication api data. $courseimage = course_get_courseimage($data); // This nasty logic is here because of hide course doesn't pass anything in the data object. if (!empty($data->communicationroomname)) { $communicationroomname = $data->communicationroomname; } else { $communicationroomname = $data->fullname ?? $oldcourse->fullname; } // Update communication room membership of enrolled users. require_once($CFG->libdir . '/enrollib.php'); $courseusers = enrol_get_course_users($data->id); $enrolledusers = []; foreach ($courseusers as $user) { $enrolledusers[] = $user->id; } // Existing communication provider. $communication = \core_communication\api::load_by_instance( context: $context, component: 'core_course', instancetype: 'coursecommunication', instanceid: $data->id, ); $existingprovider = $communication->get_provider(); $addusersrequired = false; $enablenewprovider = false; $instanceexists = true; // Action required changes if provider has changed. if ($provider !== $existingprovider) { // Provider changed, flag new one to be enabled. $enablenewprovider = true; // If provider set to none, remove all the members from previous provider. if ($provider === 'none' && $existingprovider !== '') { $communication->remove_members_from_room($enrolledusers); } else if ( // If previous provider was not none and current provider is not none, // remove members from previous provider. $existingprovider !== '' && $existingprovider !== 'none' ) { $communication->remove_members_from_room($enrolledusers); $addusersrequired = true; } else if ( // If previous provider was none and current provider is not none, // remove members from previous provider. ($existingprovider === '' || $existingprovider === 'none') ) { $addusersrequired = true; } // Disable previous provider, if one was enabled. if ($existingprovider !== '' && $existingprovider !== 'none') { $communication->update_room( active: \core_communication\processor::PROVIDER_INACTIVE, ); } // Switch to the newly selected provider so it can be updated. if ($provider !== 'none') { $communication = \core_communication\api::load_by_instance( context: $context, component: 'core_course', instancetype: 'coursecommunication', instanceid: $data->id, provider: $provider, ); // Create it if it does not exist. if ($communication->get_provider() === '') { $communication->create_and_configure_room( communicationroomname: $communicationroomname, avatar: $courseimage, instance: $data ); $communication = \core_communication\api::load_by_instance( context: $context, component: 'core_course', instancetype: 'coursecommunication', instanceid: $data->id, provider: $provider, ); $addusersrequired = true; $instanceexists = false; } } } if ($provider !== 'none' && $instanceexists) { // Update the currently enabled provider's room data. // Newly created providers do not need to run this, the create process handles it. $communication->update_room( active: $enablenewprovider ? \core_communication\processor::PROVIDER_ACTIVE : null, communicationroomname: $communicationroomname, avatar: $courseimage, instance: $data, ); } // Complete room membership tasks if required. // Newly created providers complete the user mapping but do not queue the task // (it will be handled by the room creation task). if ($addusersrequired) { $communication->add_members_to_room($enrolledusers, $instanceexists); } } // Update custom fields if there are any of them in the form. $handler = core_course\customfield\course_handler::create(); $handler->instance_form_save($data); // Update with the new data $DB->update_record('course', $data); // make sure the modinfo cache is reset rebuild_course_cache($data->id); // Purge course image cache in case if course image has been updated. \cache::make('core', 'course_image')->delete($data->id); // update course format options with full course data course_get_format($data->id)->update_course_format_options($data, $oldcourse); $course = $DB->get_record('course', array('id'=>$data->id)); if ($movecat) { $newparent = context_coursecat::instance($course->category); $context->update_moved($newparent); } $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder)); if ($fixcoursesortorder) { fix_course_sortorder(); } // purge appropriate caches in case fix_course_sortorder() did not change anything cache_helper::purge_by_event('changesincourse'); if ($changesincoursecat) { cache_helper::purge_by_event('changesincoursecat'); } // Test for and remove blocks which aren't appropriate anymore blocks_remove_inappropriate($course); // Save any custom role names. save_local_role_names($course->id, $data); // update enrol settings enrol_course_updated(false, $course, $data); // Update course tags. if (isset($data->tags)) { core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); } // Trigger a course updated event. $event = \core\event\course_updated::create(array( 'objectid' => $course->id, 'context' => context_course::instance($course->id), 'other' => array('shortname' => $course->shortname, 'fullname' => $course->fullname, 'updatedfields' => $updatedfields) )); $event->trigger(); if ($oldcourse->format !== $course->format) { // Remove all options stored for the previous format // We assume that new course format migrated everything it needed watching trigger // 'course_updated' and in method format_XXX::update_course_format_options() $DB->delete_records('course_format_options', array('courseid' => $course->id, 'format' => $oldcourse->format)); } } /** * Calculate the average number of enrolled participants per course. * * This is intended for statistics purposes during the site registration. Only visible courses are taken into account. * Front page enrolments are excluded. * * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions. * @param int $lastloginsince If specified, count only users who logged in after this timestamp. * @return float */ function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float { global $DB; $params = []; $sql = "SELECT DISTINCT ue.userid, e.courseid FROM {user_enrolments} ue JOIN {enrol} e ON e.id = ue.enrolid JOIN {course} c ON c.id = e.courseid "; if ($onlyactive || $lastloginsince) { $sql .= "JOIN {user} u ON u.id = ue.userid "; } $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 "; if ($onlyactive) { $sql .= "AND ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2) "; // Same as in the enrollib - the rounding should help caching in the database. $now = round(time(), -2); $params += [ 'active' => ENROL_USER_ACTIVE, 'enabled' => ENROL_INSTANCE_ENABLED, 'now1' => $now, 'now2' => $now, ]; } if ($lastloginsince) { $sql .= "AND u.lastlogin > :lastlogin "; $params['lastlogin'] = $lastloginsince; } $sql = "SELECT COUNT(*) FROM ($sql) total"; $enrolmenttotal = $DB->count_records_sql($sql, $params); // Get the number of visible courses (exclude the front page). $coursetotal = $DB->count_records('course', ['visible' => 1]); $coursetotal = $coursetotal - 1; if (empty($coursetotal)) { $participantaverage = 0; } else { $participantaverage = $enrolmenttotal / $coursetotal; } return $participantaverage; } /** * Average number of course modules * @return integer */ function average_number_of_courses_modules() { global $DB, $SITE; //count total of visible course module (except front page) $sql = 'SELECT COUNT(*) FROM ( SELECT cm.course, cm.module FROM {course} c, {course_modules} cm WHERE c.id = cm.course AND c.id <> :siteid AND cm.visible = 1 AND c.visible = 1) total'; $params = array('siteid' => $SITE->id); $moduletotal = $DB->count_records_sql($sql, $params); //count total of visible courses (minus front page) $coursetotal = $DB->count_records('course', array('visible' => 1)); $coursetotal = $coursetotal - 1 ; //average of course module if (empty($coursetotal)) { $coursemoduleaverage = 0; } else { $coursemoduleaverage = $moduletotal / $coursetotal; } 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); $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, array('context' => context_course::instance($course->id))); $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id; $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 * @param context $parentcontext Block's parent context * @param context $currentcontext Current context of block * @return array array of page types */ function course_page_type_list($pagetype, $parentcontext, $currentcontext) { if ($pagetype === 'course-index' || $pagetype === 'course-index-category') { // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page $pagetypes = array('*' => get_string('page-x', 'pagetype'), 'course-index-*' => get_string('page-course-index-x', 'pagetype'), ); } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) { // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.) $pagetypes = array('*' => get_string('page-x', 'pagetype')); } else { // Otherwise consider it a page inside a course even if $currentcontext is null $pagetypes = array('*' => get_string('page-x', 'pagetype'), 'course-*' => get_string('page-course-x', 'pagetype'), 'course-view-*' => get_string('page-course-view-x', 'pagetype') ); } return $pagetypes; } /** * Determine whether course ajax should be enabled for the specified course * * @param stdClass $course The course to test against * @return boolean Whether course ajax is enabled or note */ function course_ajax_enabled($course) { global $CFG, $PAGE, $SITE; // The user must be editing for AJAX to be included if (!$PAGE->user_is_editing()) { return false; } // Check that the theme suports if (!$PAGE->theme->enablecourseajax) { return false; } // Check that the course format supports ajax functionality // The site 'format' doesn't have information on course format support if ($SITE->id !== $course->id) { $courseformatajaxsupport = course_format_ajax_support($course->format); if (!$courseformatajaxsupport->capable) { return false; } } // All conditions have been met so course ajax should be enabled return true; } /** * Include the relevant javascript and language strings for the resource * toolbox YUI module * * @param integer $id The ID of the course being applied to * @param array $usedmodules An array containing the names of the modules in use on the page * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site * @param stdClass $config An object containing configuration parameters for ajax modules including: * * resourceurl The URL to post changes to for resource changes * * sectionurl The URL to post changes to for section changes * * pageparams Additional parameters to pass through in the post * @return bool */ function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) { global $CFG, $PAGE, $SITE; // Init the course editor module to support UI components. $format = course_get_format($course); include_course_editor($format); // Ensure that ajax should be included if (!course_ajax_enabled($course)) { return false; } // Component based formats don't use YUI drag and drop anymore. if (!$format->supports_components() && course_format_uses_sections($course->format)) { if (!$config) { $config = new stdClass(); } // The URL to use for resource changes. if (!isset($config->resourceurl)) { $config->resourceurl = '/course/rest.php'; } // The URL to use for section changes. if (!isset($config->sectionurl)) { $config->sectionurl = '/course/rest.php'; } // Any additional parameters which need to be included on page submission. if (!isset($config->pageparams)) { $config->pageparams = array(); } $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop', array(array( 'courseid' => $course->id, 'ajaxurl' => $config->sectionurl, 'config' => $config, )), null, true); $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop', array(array( 'courseid' => $course->id, 'ajaxurl' => $config->resourceurl, 'config' => $config, )), null, true); // Require various strings for the command toolbox. $PAGE->requires->strings_for_js(array( 'moveleft', 'deletechecktype', 'deletechecktypename', 'edittitle', 'edittitleinstructions', 'show', 'hide', 'highlight', 'highlightoff', 'groupsnone', 'groupsvisible', 'groupsseparate', 'markthistopic', 'markedthistopic', 'movesection', 'movecoursemodule', 'movecoursesection', 'movecontent', 'tocontent', 'emptydragdropregion', 'afterresource', 'aftersection', 'totopofsection', ), 'moodle'); // Include section-specific strings for formats which support sections. if (course_format_uses_sections($course->format)) { $PAGE->requires->strings_for_js(array( 'showfromothers', 'hidefromothers', ), 'format_' . $course->format); } // For confirming resource deletion we need the name of the module in question. foreach ($usedmodules as $module => $modname) { $PAGE->requires->string_for_js('pluginname', $module); } // Load drag and drop upload AJAX. require_once($CFG->dirroot.'/course/dnduploadlib.php'); dndupload_add_to_course($course, $enabledmodules); } $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format)); return true; } /** * Include and configure the course editor modules. * * @param course_format $format the course format instance. */ function include_course_editor(course_format $format) { global $PAGE, $SITE; $course = $format->get_course(); if ($SITE->id === $course->id) { return; } $statekey = course_format::session_cache($course); // Edition mode and some format specs must be passed to the init method. $setup = (object)[ 'editing' => $format->show_editor(), 'supportscomponents' => $format->supports_components(), 'statekey' => $statekey, 'overriddenStrings' => $format->get_editor_custom_strings(), ]; // All the new editor elements will be loaded after the course is presented and // the initial course state will be generated using core_course_get_state webservice. $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]); } /** * Returns the sorted list of available course formats, filtered by enabled if necessary * * @param bool $enabledonly return only formats that are enabled * @return array array of sorted format names */ function get_sorted_course_formats($enabledonly = false) { global $CFG; $formats = core_component::get_plugin_list('format'); if (!empty($CFG->format_plugins_sortorder)) { $order = explode(',', $CFG->format_plugins_sortorder); $order = array_merge(array_intersect($order, array_keys($formats)), array_diff(array_keys($formats), $order)); } else { $order = array_keys($formats); } if (!$enabledonly) { return $order; } $sortedformats = array(); foreach ($order as $formatname) { if (!get_config('format_'.$formatname, 'disabled')) { $sortedformats[] = $formatname; } } return $sortedformats; } /** * The URL to use for the specified course (with section) * * @param int|stdClass $courseorid The course to get the section name for (either object or just course id) * @param int|stdClass $section Section object from database or just field course_sections.section * if omitted the course view page is returned * @param array $options options for view URL. At the moment core uses: * 'navigation' (bool) if true and section has no separate page, the function returns null * 'sr' (int) used by multipage formats to specify to which section to return * @return moodle_url The url of course */ function course_get_url($courseorid, $section = null, $options = array()) { return course_get_format($courseorid)->get_view_url($section, $options); } /** * Create a module. * * It includes: * - capability checks and other checks * - create the module from the module info * * @param object $module * @return object the created module info * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course */ function create_module($moduleinfo) { global $DB, $CFG; require_once($CFG->dirroot . '/course/modlib.php'); // Check manadatory attributs. $mandatoryfields = array('modulename', 'course', 'section', 'visible'); if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) { $mandatoryfields[] = 'introeditor'; } foreach($mandatoryfields as $mandatoryfield) { if (!isset($moduleinfo->{$mandatoryfield})) { throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield); } } // Some additional checks (capability / existing instances). $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST); list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section); // Add the module. $moduleinfo->module = $module->id; $moduleinfo = add_moduleinfo($moduleinfo, $course, null); return $moduleinfo; } /** * Update a module. * * It includes: * - capability and other checks * - update the module * * @param object $module * @return object the updated module info * @throws moodle_exception if current user is not allowed to update the module */ function update_module($moduleinfo) { global $DB, $CFG; require_once($CFG->dirroot . '/course/modlib.php'); // Check the course module exists. $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST); // Check the course exists. $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); // Some checks (capaibility / existing instances). list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm); // Retrieve few information needed by update_moduleinfo. $moduleinfo->modulename = $cm->modname; if (!isset($moduleinfo->scale)) { $moduleinfo->scale = 0; } $moduleinfo->type = 'mod'; // Update the module. list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null); return $moduleinfo; } /** * Duplicate a module on the course for ajax. * * @see mod_duplicate_module() * @param object $course The course * @param object $cm The course module to duplicate * @param int $sr The section to link back to (used for creating the links) * @throws moodle_exception if the plugin doesn't support duplication * @return Object containing: * - fullcontent: The HTML markup for the created CM * - cmid: The CMID of the newly created CM * - redirect: Whether to trigger a redirect following this change */ function mod_duplicate_activity($course, $cm, $sr = null) { global $PAGE; $newcm = duplicate_module($course, $cm); $resp = new stdClass(); if ($newcm) { $format = course_get_format($course); $renderer = $format->get_renderer($PAGE); $modinfo = $format->get_modinfo(); $section = $modinfo->get_section_info($newcm->sectionnum); // Get the new element html content. $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm); $resp->cmid = $newcm->id; } else { // Trigger a redirect. $resp->redirect = true; } return $resp; } /** * Api to duplicate a module. * * @param object $course course object. * @param object $cm course module object to be duplicated. * @param int $sectionid section ID new course module will be placed in. * @param bool $changename updates module name with text from duplicatedmodule lang string. * @since Moodle 2.8 * * @throws Exception * @throws coding_exception * @throws moodle_exception * @throws restore_controller_exception * * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm. */ function duplicate_module($course, $cm, int $sectionid = null, bool $changename = true): ?cm_info { global $CFG, $DB, $USER; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->libdir . '/filelib.php'); $a = new stdClass(); $a->modtype = get_string('modulename', $cm->modname); $a->modname = format_string($cm->name); if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) { throw new moodle_exception('duplicatenosupport', 'error', '', $a); } // Backup the activity. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $backupbasepath = $bc->get_plan()->get_basepath(); $bc->execute_plan(); $bc->destroy(); // Restore the backup immediately. $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); // Make sure that the restore_general_groups setting is always enabled when duplicating an activity. $plan = $rc->get_plan(); $groupsetting = $plan->get_setting('groups'); if (empty($groupsetting->get_value())) { $groupsetting->set_value(true); } $cmcontext = context_module::instance($cm->id); if (!$rc->execute_precheck()) { $precheckresults = $rc->get_precheck_results(); if (is_array($precheckresults) && !empty($precheckresults['errors'])) { if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } } } $rc->execute_plan(); // Now a bit hacky part follows - we try to get the cmid of the newly // restored copy of the module. $newcmid = null; $tasks = $rc->get_plan()->get_tasks(); foreach ($tasks as $task) { if (is_subclass_of($task, 'restore_activity_task')) { if ($task->get_old_contextid() == $cmcontext->id) { $newcmid = $task->get_moduleid(); break; } } } $rc->destroy(); if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($backupbasepath); } // If we know the cmid of the new course module, let us move it // right below the original one. otherwise it will stay at the // end of the section. if ($newcmid) { // Proceed with activity renaming before everything else. We don't use APIs here to avoid // triggering a lot of create/update duplicated events. $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course); if ($changename) { // Add ' (copy)' language string postfix to duplicated module. $newname = get_string('duplicatedmodule', 'moodle', $newcm->name); set_coursemodule_name($newcm->id, $newname); } $section = $DB->get_record('course_sections', ['id' => $sectionid ?? $cm->section, 'course' => $cm->course]); if (isset($sectionid)) { moveto_module($newcm, $section); } else { $modarray = explode(",", trim($section->sequence)); $cmindex = array_search($cm->id, $modarray); if ($cmindex !== false && $cmindex < count($modarray) - 1) { moveto_module($newcm, $section, $modarray[$cmindex + 1]); } } // Update calendar events with the duplicated module. // The following line is to be removed in MDL-58906. course_module_update_calendar_events($newcm->modname, null, $newcm); // Trigger course module created event. We can trigger the event only if we know the newcmid. $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid); $event = \core\event\course_module_created::create_from_cm($newcm); $event->trigger(); } return isset($newcm) ? $newcm : null; } /** * Compare two objects to find out their correct order based on timestamp (to be used by usort). * Sorts by descending order of time. * * @param stdClass $a First object * @param stdClass $b Second object * @return int 0,1,-1 representing the order */ function compare_activities_by_time_desc($a, $b) { // Make sure the activities actually have a timestamp property. if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return 0; } // We treat instances without timestamp as if they have a timestamp of 0. if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) { return 1; } if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return -1; } if ($a->timestamp == $b->timestamp) { return 0; } return ($a->timestamp > $b->timestamp) ? -1 : 1; } /** * Compare two objects to find out their correct order based on timestamp (to be used by usort). * Sorts by ascending order of time. * * @param stdClass $a First object * @param stdClass $b Second object * @return int 0,1,-1 representing the order */ function compare_activities_by_time_asc($a, $b) { // Make sure the activities actually have a timestamp property. if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return 0; } // We treat instances without timestamp as if they have a timestamp of 0. if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) { return -1; } if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { return 1; } if ($a->timestamp == $b->timestamp) { return 0; } return ($a->timestamp < $b->timestamp) ? -1 : 1; } /** * Changes the visibility of a course. * * @param int $courseid The course to change. * @param bool $show True to make it visible, false otherwise. * @return bool */ function course_change_visibility($courseid, $show = true) { $course = new stdClass; $course->id = $courseid; $course->visible = ($show) ? '1' : '0'; $course->visibleold = $course->visible; update_course($course); return true; } /** * Changes the course sortorder by one, moving it up or down one in respect to sort order. * * @param stdClass|core_course_list_element $course * @param bool $up If set to true the course will be moved up one. Otherwise down one. * @return bool */ function course_change_sortorder_by_one($course, $up) { global $DB; $params = array($course->sortorder, $course->category); if ($up) { $select = 'sortorder < ? AND category = ?'; $sort = 'sortorder DESC'; } else { $select = 'sortorder > ? AND category = ?'; $sort = 'sortorder ASC'; } fix_course_sortorder(); $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1); if ($swapcourse) { $swapcourse = reset($swapcourse); $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id)); $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id)); // Finally reorder courses. fix_course_sortorder(); cache_helper::purge_by_event('changesincourse'); return true; } return false; } /** * Changes the sort order of courses in a category so that the first course appears after the second. * * @param int|stdClass $courseorid The course to focus on. * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category. * @return bool */ function course_change_sortorder_after_course($courseorid, $moveaftercourseid) { global $DB; if (!is_object($courseorid)) { $course = get_course($courseorid); } else { $course = $courseorid; } if ((int)$moveaftercourseid === 0) { // We've moving the course to the start of the queue. $sql = 'SELECT sortorder FROM {course} WHERE category = :categoryid ORDER BY sortorder'; $params = array( 'categoryid' => $course->category ); $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE); $sql = 'UPDATE {course} SET sortorder = sortorder + 1 WHERE category = :categoryid AND id <> :id'; $params = array( 'categoryid' => $course->category, 'id' => $course->id, ); $DB->execute($sql, $params); $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id)); } else if ($course->id === $moveaftercourseid) { // They're the same - moronic. debugging("Invalid move after course given.", DEBUG_DEVELOPER); return false; } else { // Moving this course after the given course. It could be before it could be after. $moveaftercourse = get_course($moveaftercourseid); if ($course->category !== $moveaftercourse->category) { debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER); return false; } // Increment all courses in the same category that are ordered after the moveafter course. // This makes a space for the course we're moving. $sql = 'UPDATE {course} SET sortorder = sortorder + 1 WHERE category = :categoryid AND sortorder > :sortorder'; $params = array( 'categoryid' => $moveaftercourse->category, 'sortorder' => $moveaftercourse->sortorder ); $DB->execute($sql, $params); $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id)); } fix_course_sortorder(); cache_helper::purge_by_event('changesincourse'); return true; } /** * Trigger course viewed event. This API function is used when course view actions happens, * usually in course/view.php but also in external functions. * * @param stdClass $context course context object * @param int $sectionnumber section number * @since Moodle 2.9 */ function course_view($context, $sectionnumber = 0) { $eventdata = array('context' => $context); if (!empty($sectionnumber)) { $eventdata['other']['coursesectionnumber'] = $sectionnumber; } $event = \core\event\course_viewed::create($eventdata); $event->trigger(); user_accesstime_log($context->instanceid); } /** * Returns courses tagged with a specified tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromctx context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $ctx context id where to search for records * @param bool $rec search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { global $CFG, $PAGE; $perpage = $exclusivemode ? $CFG->coursesperpage : 5; $displayoptions = array( 'limit' => $perpage, 'offset' => $page * $perpage, 'viewmoreurl' => null, ); $courserenderer = $PAGE->get_renderer('core', 'course'); $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec)); $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions); $totalpages = ceil($totalcount / $perpage); return new core_tag\output\tagindex($tag, 'core', 'course', $content, $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); } /** * Implements callback inplace_editable() allowing to edit values in-place * * @param string $itemtype * @param int $itemid * @param mixed $newvalue * @return \core\output\inplace_editable */ function core_course_inplace_editable($itemtype, $itemid, $newvalue) { if ($itemtype === 'activityname') { return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue); } } /** * This function calculates the minimum and maximum cutoff values for the timestart of * the given event. * * It will return an array with two values, the first being the minimum cutoff value and * the second being the maximum cutoff value. Either or both values can be null, which * indicates there is no minimum or maximum, respectively. * * If a cutoff is required then the function must return an array containing the cutoff * timestamp and error string to display to the user if the cutoff value is violated. * * A minimum and maximum cutoff return value will look like: * [ * [1505704373, 'The date must be after this date'], * [1506741172, 'The date must be before this date'] * ] * * @param calendar_event $event The calendar event to get the time range for * @param stdClass $course The course object to get the range from * @return array Returns an array with min and max date. */ function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) { $mindate = null; $maxdate = null; if ($course->startdate) { $mindate = [ $course->startdate, get_string('errorbeforecoursestart', 'calendar') ]; } return [$mindate, $maxdate]; } /** * Render the message drawer to be included in the top of the body of each page. * * @return string HTML */ function core_course_drawer(): string { global $PAGE; // Only add course index on non-site course pages. if (!$PAGE->course || $PAGE->course->id == SITEID) { return ''; } // Show course index to users can access the course only. if (!can_access_course($PAGE->course, null, '', true)) { return ''; } $format = course_get_format($PAGE->course); $renderer = $format->get_renderer($PAGE); if (method_exists($renderer, 'course_index_drawer')) { return $renderer->course_index_drawer($format); } return ''; } /** * Returns course modules tagged with a specified tag ready for output on tag/index.php page * * This is a callback used by the tag area core/course_modules to search for course modules * tagged with a specific tag. * * @param core_tag_tag $tag * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag * are displayed on the page and the per-page limit may be bigger * @param int $fromcontextid context id where the link was displayed, may be used by callbacks * to display items in the same context first * @param int $contextid context id where to search for records * @param bool $recursivecontext search in subcontexts as well * @param int $page 0-based number of page being displayed * @return \core_tag\output\tagindex */ function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0, $recursivecontext = 1, $page = 0) { global $OUTPUT; $perpage = $exclusivemode ? 20 : 5; // Build select query. $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect FROM {course_modules} cm JOIN {tag_instance} tt ON cm.id = tt.itemid JOIN {course} c ON cm.course = c.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component AND cm.deletioninprogress = 0 AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%"; $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core', 'coursemodulecontextlevel' => CONTEXT_MODULE); if ($contextid) { $context = context::instance_by_id($contextid); $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; $params['contextid'] = $context->id; $params['path'] = $context->path.'/%'; } $query .= ' ORDER BY'; if ($fromcontextid) { // In order-clause specify that modules from inside "fromctx" context should be returned first. $fromcontext = context::instance_by_id($fromcontextid); $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; $params['fromcontextid'] = $fromcontext->id; $params['frompath'] = $fromcontext->path.'/%'; } $query .= ' c.sortorder, cm.id'; $totalpages = $page + 1; // Use core_tag_index_builder to build and filter the list of items. // Request one item more than we need so we know if next page exists. $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1); while ($item = $builder->has_item_that_needs_access_check()) { context_helper::preload_from_record($item); $courseid = $item->courseid; if (!$builder->can_access_course($courseid)) { $builder->set_accessible($item, false); continue; } $modinfo = get_fast_modinfo($builder->get_course($courseid)); // Set accessibility of this item and all other items in the same course. $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { if ($taggeditem->courseid == $courseid) { $cm = $modinfo->get_cm($taggeditem->cmid); $builder->set_accessible($taggeditem, $cm->uservisible); } }); } $items = $builder->get_items(); if (count($items) > $perpage) { $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. array_pop($items); } // Build the display contents. if ($items) { $tagfeed = new core_tag\output\tagfeed(); foreach ($items as $item) { context_helper::preload_from_record($item); $course = $builder->get_course($item->courseid); $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($item->cmid); $courseurl = course_get_url($item->courseid, $cm->sectionnum); $cmname = $cm->get_formatted_name(); if (!$exclusivemode) { $cmname = shorten_text($cmname, 100); } $cmname = html_writer::link($cm->url?:$courseurl, $cmname); $coursename = format_string($course->fullname, true, array('context' => context_course::instance($item->courseid))); $coursename = html_writer::link($courseurl, $coursename); $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url())); $tagfeed->add($icon, $cmname, $coursename); } $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT)); return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content, $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages); } } /** * Return an object with the list of navigation options in a course that are avaialable or not for the current user. * This function also handles the frontpage course. * * @param stdClass $context context object (it can be a course context or the system context for frontpage settings) * @param stdClass $course the course where the settings are being rendered * @return stdClass the navigation options in a course and their availability status * @since Moodle 3.2 */ function course_get_user_navigation_options($context, $course = null) { global $CFG, $USER; $isloggedin = isloggedin(); $isguestuser = isguestuser(); $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM; if ($isfrontpage) { $sitecontext = $context; } else { $sitecontext = context_system::instance(); } // Sets defaults for all options. $options = (object) [ 'badges' => false, 'blogs' => false, 'competencies' => false, 'grades' => false, 'notes' => false, 'participants' => false, 'search' => false, 'tags' => false, 'communication' => false, ]; $options->blogs = !empty($CFG->enableblogs) && ($CFG->bloglevel == BLOG_GLOBAL_LEVEL || ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser))) && has_capability('moodle/blog:view', $sitecontext); $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context); // Frontpage settings? if ($isfrontpage) { // We are on the front page, so make sure we use the proper capability (site:viewparticipants). $options->participants = course_can_view_participants($sitecontext); $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext); $options->tags = !empty($CFG->usetags) && $isloggedin; $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext); } else { // We are in a course, so make sure we use the proper capability (course:viewparticipants). $options->participants = course_can_view_participants($context); // Only display badges if they are enabled and the current user can manage them or if they can view them and have, // at least, one available badge. if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) { $canmanage = has_any_capability([ 'moodle/badges:createbadge', 'moodle/badges:awardbadge', 'moodle/badges:configurecriteria', 'moodle/badges:configuremessages', 'moodle/badges:configuredetails', 'moodle/badges:deletebadge', ], $context ); $totalbadges = []; $canview = false; if (!$canmanage) { // This only needs to be calculated if the user can't manage badges (to improve performance). $canview = has_capability('moodle/badges:viewbadges', $context); if ($canview) { require_once($CFG->dirroot.'/lib/badgeslib.php'); if (is_null($course)) { $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id)); } else { $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id)); } } } $options->badges = ($canmanage || ($canview && $totalbadges > 0)); } // Add view grade report is permitted. $grades = false; if (has_capability('moodle/grade:viewall', $context)) { $grades = true; } else if (!empty($course->showgrades)) { $reports = core_component::get_plugin_list('gradereport'); if (is_array($reports) && count($reports) > 0) { // Get all installed reports. arsort($reports); // User is last, we want to test it first. foreach ($reports as $plugin => $plugindir) { if (has_capability('gradereport/'.$plugin.':view', $context)) { // Stop when the first visible plugin is found. $grades = true; break; } } } } $options->grades = $grades; } if (\core_communication\api::is_available()) { $options->communication = has_capability('moodle/course:configurecoursecommunication', $context); } if (\core_competency\api::is_enabled()) { $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); $options->competencies = has_any_capability($capabilities, $context); } return $options; } /** * Return an object with the list of administration options in a course that are available or not for the current user. * This function also handles the frontpage settings. * * @param stdClass $course course object (for frontpage it should be a clone of $SITE) * @param stdClass $context context object (course context) * @return stdClass the administration options in a course and their availability status * @since Moodle 3.2 */ function course_get_user_administration_options($course, $context) { global $CFG; $isfrontpage = $course->id == SITEID; $completionenabled = $CFG->enablecompletion && $course->enablecompletion; $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0; $options = new stdClass; $options->update = has_capability('moodle/course:update', $context); $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion && ($options->update || $hascompletionoptions); $options->filters = has_capability('moodle/filter:manage', $context) && count(filter_get_available_in_context($context)) > 0; $options->reports = has_capability('moodle/site:viewreports', $context); $options->backup = has_capability('moodle/backup:backupcourse', $context); $options->restore = has_capability('moodle/restore:restorecourse', $context); $options->copy = \core_course\management\helper::can_copy_course($course->id); $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context)); if (!$isfrontpage) { $options->tags = has_capability('moodle/course:tag', $context); $options->gradebook = has_capability('moodle/grade:manage', $context); $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context); $options->badges = !empty($CFG->enablebadges); $options->import = has_capability('moodle/restore:restoretargetimport', $context); $options->reset = has_capability('moodle/course:reset', $context); $options->roles = has_capability('moodle/role:switchroles', $context); } else { // Set default options to false. $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades'); foreach ($listofoptions as $option) { $options->$option = false; } } return $options; } /** * Validates course start and end dates. * * Checks that the end course date is not greater than the start course date. * * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input. * * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input. * @return mixed False if everything alright, error codes otherwise. */ function course_validate_dates($coursedata) { // If both start and end dates are set end date should be later than the start date. if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) && ($coursedata['enddate'] < $coursedata['startdate'])) { return 'enddatebeforestartdate'; } // If start date is not set end date can not be set. if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) { return 'nostartdatenoenddate'; } return false; } /** * Check for course updates in the given context level instances (only modules supported right Now) * * @param stdClass $course course object * @param array $tocheck instances to check for updates * @param array $filter check only for updates in these areas * @return array list of warnings and instances with updates information * @since Moodle 3.2 */ function course_check_updates($course, $tocheck, $filter = array()) { global $CFG, $DB; $instances = array(); $warnings = array(); $modulescallbacksupport = array(); $modinfo = get_fast_modinfo($course); $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since'); // Check instances. foreach ($tocheck as $instance) { if ($instance['contextlevel'] == 'module') { // Check module visibility. try { $cm = $modinfo->get_cm($instance['id']); } catch (Exception $e) { $warnings[] = array( 'item' => 'module', 'itemid' => $instance['id'], 'warningcode' => 'cmidnotincourse', 'message' => 'This module id does not belong to this course.' ); continue; } if (!$cm->uservisible) { $warnings[] = array( 'item' => 'module', 'itemid' => $instance['id'], 'warningcode' => 'nonuservisible', 'message' => 'You don\'t have access to this module.' ); continue; } if (empty($supportedplugins['mod_' . $cm->modname])) { $warnings[] = array( 'item' => 'module', 'itemid' => $instance['id'], 'warningcode' => 'missingcallback', 'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'], ); continue; } // Retrieve the module instance. $instances[] = array( 'contextlevel' => $instance['contextlevel'], 'id' => $instance['id'], 'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter) ); } else { $warnings[] = array( 'item' => 'contextlevel', 'itemid' => $instance['id'], 'warningcode' => 'contextlevelnotsupported', 'message' => 'Context level not yet supported ' . $instance['contextlevel'], ); } } return array($instances, $warnings); } /** * This function classifies a course as past, in progress or future. * * This function may incur a DB hit to calculate course completion. * @param stdClass $course Course record * @param stdClass $user User record (optional - defaults to $USER). * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required). * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST) */ function course_classify_for_timeline($course, $user = null, $completioninfo = null) { global $USER; if ($user == null) { $user = $USER; } if ($completioninfo == null) { $completioninfo = new completion_info($course); } // Let plugins override data for timeline classification. $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php'); foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($course, $user, $completioninfo); } } $today = time(); // End date past. if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) { return COURSE_TIMELINE_PAST; } // Course was completed. if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) { return COURSE_TIMELINE_PAST; } // Start date not reached. if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) { return COURSE_TIMELINE_FUTURE; } // Everything else is in progress. return COURSE_TIMELINE_INPROGRESS; } /** * This function calculates the end date to use for display classification purposes, * incorporating the grace period, if any. * * @param stdClass $course The course record. * @return int The new enddate. */ function course_classify_end_date($course) { global $CFG; $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter; $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days"); return $enddate->getTimestamp(); } /** * This function calculates the start date to use for display classification purposes, * incorporating the grace period, if any. * * @param stdClass $course The course record. * @return int The new startdate. */ function course_classify_start_date($course) { global $CFG; $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore; $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days"); return $startdate->getTimestamp(); } /** * Group a list of courses into either past, future, or in progress. * * The return value will be an array indexed by the COURSE_TIMELINE_* constants * with each value being an array of courses in that group. * E.g. * [ * COURSE_TIMELINE_PAST => [... list of past courses ...], * COURSE_TIMELINE_FUTURE => [], * COURSE_TIMELINE_INPROGRESS => [] * ] * * @param array $courses List of courses to be grouped. * @return array */ function course_classify_courses_for_timeline(array $courses) { return array_reduce($courses, function($carry, $course) { $classification = course_classify_for_timeline($course); array_push($carry[$classification], $course); return $carry; }, [ COURSE_TIMELINE_PAST => [], COURSE_TIMELINE_FUTURE => [], COURSE_TIMELINE_INPROGRESS => [] ]); } /** * Get the list of enrolled courses for the current user. * * This function returns a Generator. The courses will be loaded from the database * in chunks rather than a single query. * * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @param string|null $fields SQL string for fields to be returned * @param int $dbquerylimit The number of records to load per DB request * @param array $includecourses courses ids to be restricted * @param array $hiddencourses courses ids to be excluded * @return Generator */ function course_get_enrolled_courses_for_logged_in_user( int $limit = 0, int $offset = 0, string $sort = null, string $fields = null, int $dbquerylimit = COURSE_DB_QUERY_LIMIT, array $includecourses = [], array $hiddencourses = [] ) : Generator { $haslimit = !empty($limit); $recordsloaded = 0; $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit; while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) { yield from $courses; $recordsloaded += $querylimit; if (count($courses) < $querylimit) { break; } if ($haslimit && $recordsloaded >= $limit) { break; } $offset += $querylimit; } } /** * Get the list of enrolled courses the current user searched for. * * This function returns a Generator. The courses will be loaded from the database * in chunks rather than a single query. * * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @param string|null $fields SQL string for fields to be returned * @param int $dbquerylimit The number of records to load per DB request * @param array $searchcriteria contains search criteria * @param array $options display options, same as in get_courses() except 'recursive' is ignored - * search is always category-independent * @return Generator */ function course_get_enrolled_courses_for_logged_in_user_from_search( int $limit = 0, int $offset = 0, string $sort = null, string $fields = null, int $dbquerylimit = COURSE_DB_QUERY_LIMIT, array $searchcriteria = [], array $options = [] ) : Generator { $haslimit = !empty($limit); $recordsloaded = 0; $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit; $ids = core_course_category::search_courses($searchcriteria, $options); // If no courses were found matching the criteria return back. if (empty($ids)) { return; } while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) { yield from $courses; $recordsloaded += $querylimit; if (count($courses) < $querylimit) { break; } if ($haslimit && $recordsloaded >= $limit) { break; } $offset += $querylimit; } } /** * Search the given $courses for any that match the given $classification up to the specified * $limit. * * This function will return the subset of courses that match the classification as well as the * number of courses it had to process to build that subset. * * It is recommended that for larger sets of courses this function is given a Generator that loads * the courses from the database in chunks. * * @param array|Traversable $courses List of courses to process * @param string $classification One of the COURSE_TIMELINE_* constants * @param int $limit Limit the number of results to this amount * @return array First value is the filtered courses, second value is the number of courses processed */ function course_filter_courses_by_timeline_classification( $courses, string $classification, int $limit = 0 ) : array { if (!in_array($classification, [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) { $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, ' . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE'; throw new moodle_exception($message); } $filteredcourses = []; $numberofcoursesprocessed = 0; $filtermatches = 0; foreach ($courses as $course) { $numberofcoursesprocessed++; $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0); // Added as of MDL-63457 toggle viewability for each user. if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) || $classification == COURSE_TIMELINE_SEARCH|| (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) { $filteredcourses[] = $course; $filtermatches++; } if ($limit && $filtermatches >= $limit) { // We've found the number of requested courses. No need to continue searching. break; } } // Return the number of filtered courses as well as the number of courses that were searched // in order to find the matching courses. This allows the calling code to do some kind of // pagination. return [$filteredcourses, $numberofcoursesprocessed]; } /** * Search the given $courses for any that match the given $classification up to the specified * $limit. * * This function will return the subset of courses that are favourites as well as the * number of courses it had to process to build that subset. * * It is recommended that for larger sets of courses this function is given a Generator that loads * the courses from the database in chunks. * * @param array|Traversable $courses List of courses to process * @param array $favouritecourseids Array of favourite courses. * @param int $limit Limit the number of results to this amount * @return array First value is the filtered courses, second value is the number of courses processed */ function course_filter_courses_by_favourites( $courses, $favouritecourseids, int $limit = 0 ) : array { $filteredcourses = []; $numberofcoursesprocessed = 0; $filtermatches = 0; foreach ($courses as $course) { $numberofcoursesprocessed++; if (in_array($course->id, $favouritecourseids)) { $filteredcourses[] = $course; $filtermatches++; } if ($limit && $filtermatches >= $limit) { // We've found the number of requested courses. No need to continue searching. break; } } // Return the number of filtered courses as well as the number of courses that were searched // in order to find the matching courses. This allows the calling code to do some kind of // pagination. return [$filteredcourses, $numberofcoursesprocessed]; } /** * Search the given $courses for any that have a $customfieldname value that matches the given * $customfieldvalue, up to the specified $limit. * * This function will return the subset of courses that matches the value as well as the * number of courses it had to process to build that subset. * * It is recommended that for larger sets of courses this function is given a Generator that loads * the courses from the database in chunks. * * @param array|Traversable $courses List of courses to process * @param string $customfieldname the shortname of the custom field to match against * @param string $customfieldvalue the value this custom field needs to match * @param int $limit Limit the number of results to this amount * @return array First value is the filtered courses, second value is the number of courses processed */ function course_filter_courses_by_customfield( $courses, $customfieldname, $customfieldvalue, int $limit = 0 ) : array { global $DB; if (!$courses) { return [[], 0]; } // Prepare the list of courses to search through. $coursesbyid = []; foreach ($courses as $course) { $coursesbyid[$course->id] = $course; } if (!$coursesbyid) { return [[], 0]; } list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED); // Get the id of the custom field. $sql = " SELECT f.id FROM {customfield_field} f JOIN {customfield_category} cat ON cat.id = f.categoryid WHERE f.shortname = ? AND cat.component = 'core_course' AND cat.area = 'course' "; $fieldid = $DB->get_field_sql($sql, [$customfieldname]); if (!$fieldid) { return [[], 0]; } // Get a list of courseids that match that custom field value. if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) { $comparevalue = $DB->sql_compare_text('cd.value'); $sql = " SELECT c.id FROM {course} c LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid WHERE c.id $csql AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0') "; $params['fieldid'] = $fieldid; $matchcourseids = $DB->get_fieldset_sql($sql, $params); } else { $comparevalue = $DB->sql_compare_text('value'); $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql"; $params['fieldid'] = $fieldid; $params['customfieldvalue'] = $customfieldvalue; $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params); } // Prepare the list of courses to return. $filteredcourses = []; $numberofcoursesprocessed = 0; $filtermatches = 0; foreach ($coursesbyid as $course) { $numberofcoursesprocessed++; if (in_array($course->id, $matchcourseids)) { $filteredcourses[] = $course; $filtermatches++; } if ($limit && $filtermatches >= $limit) { // We've found the number of requested courses. No need to continue searching. break; } } // Return the number of filtered courses as well as the number of courses that were searched // in order to find the matching courses. This allows the calling code to do some kind of // pagination. return [$filteredcourses, $numberofcoursesprocessed]; } /** * Check module updates since a given time. * This function checks for updates in the module config, file areas, completion, grades, comments and ratings. * * @param cm_info $cm course module data * @param int $from the time to check * @param array $fileareas additional file ares to check * @param array $filter if we need to filter and return only selected updates * @return stdClass object with the different updates * @since Moodle 3.2 */ function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) { global $DB, $CFG, $USER; $context = $cm->context; $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); $updates = new stdClass(); $course = get_course($cm->course); $component = 'mod_' . $cm->modname; // Check changes in the module configuration. if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) { $updates->configuration = (object) array('updated' => false); if ($updates->configuration->updated = $mod->timemodified > $from) { $updates->configuration->timeupdated = $mod->timemodified; } } // Check for updates in files. if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) { $fileareas[] = 'intro'; } if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) { $fs = get_file_storage(); $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from); foreach ($fileareas as $filearea) { $updates->{$filearea . 'files'} = (object) array('updated' => false); } foreach ($files as $file) { $updates->{$file->get_filearea() . 'files'}->updated = true; $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id(); } } // Check completion. $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES); $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS); if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) { $updates->completion = (object) array('updated' => false); $completion = new completion_info($course); // Use wholecourse to cache all the modules the first time. $completiondata = $completion->get_data($cm, true); if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) { $updates->completion->timemodified = $completiondata->timemodified; } } // Check grades. $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE); $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES); if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) { require_once($CFG->libdir . '/gradelib.php'); $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id); if (empty($filter) or in_array('gradeitems', $filter)) { $updates->gradeitems = (object) array('updated' => false); foreach ($grades->items as $gradeitem) { foreach ($gradeitem->grades as $grade) { if ($grade->datesubmitted > $from or $grade->dategraded > $from) { $updates->gradeitems->updated = true; $updates->gradeitems->itemids[] = $gradeitem->id; } } } } if (empty($filter) or in_array('outcomes', $filter)) { $updates->outcomes = (object) array('updated' => false); foreach ($grades->outcomes as $outcome) { foreach ($outcome->grades as $grade) { if ($grade->datesubmitted > $from or $grade->dategraded > $from) { $updates->outcomes->updated = true; $updates->outcomes->itemids[] = $outcome->id; } } } } } // Check comments. if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) { $updates->comments = (object) array('updated' => false); require_once($CFG->dirroot . '/comment/lib.php'); require_once($CFG->dirroot . '/comment/locallib.php'); $manager = new comment_manager(); $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm); if (!empty($comments)) { $updates->comments->updated = true; $updates->comments->itemids = array_keys($comments); } } // Check ratings. if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) { $updates->ratings = (object) array('updated' => false); require_once($CFG->dirroot . '/rating/lib.php'); $manager = new rating_manager(); $ratings = $manager->get_component_ratings_since($context, $component, $from); if (!empty($ratings)) { $updates->ratings->updated = true; $updates->ratings->itemids = array_keys($ratings); } } return $updates; } /** * Returns true if the user can view the participant page, false otherwise, * * @param context $context The context we are checking. * @return bool */ function course_can_view_participants($context) { $viewparticipantscap = 'moodle/course:viewparticipants'; if ($context->contextlevel == CONTEXT_SYSTEM) { $viewparticipantscap = 'moodle/site:viewparticipants'; } return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context); } /** * Checks if a user can view the participant page, if not throws an exception. * * @param context $context The context we are checking. * @throws required_capability_exception */ function course_require_view_participants($context) { if (!course_can_view_participants($context)) { $viewparticipantscap = 'moodle/course:viewparticipants'; if ($context->contextlevel == CONTEXT_SYSTEM) { $viewparticipantscap = 'moodle/site:viewparticipants'; } throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', ''); } } /** * Return whether the user can download from the specified backup file area in the given context. * * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'. * @param \context $context * @param stdClass $user the user object. If not provided, the current user will be checked. * @return bool true if the user is allowed to download in the context, false otherwise. */ function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) { $candownload = false; switch ($filearea) { case 'course': case 'backup': $candownload = has_capability('moodle/backup:downloadfile', $context, $user); break; case 'automated': // Given the automated backups may contain userinfo, we restrict access such that only users who are able to // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo' // doesn't make sense here. $candownload = has_capability('moodle/backup:downloadfile', $context, $user) && has_capability('moodle/restore:userinfo', $context, $user); break; default: break; } return $candownload; } /** * Get a list of hidden courses * * @param int|object|null $user User override to get the filter from. Defaults to current user * @return array $ids List of hidden courses * @throws coding_exception */ function get_hidden_courses_on_timeline($user = null) { global $USER; if (empty($user)) { $user = $USER->id; } $preferences = get_user_preferences(null, null, $user); $ids = []; foreach ($preferences as $key => $value) { if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) { $id = preg_split('/block_myoverview_hidden_course_/', $key); $ids[] = $id[1]; } } return $ids; } /** * Returns a list of the most recently courses accessed by a user * * @param int $userid User id from which the courses will be obtained * @param int $limit Restrict result set to this amount * @param int $offset Skip this number of records from the start of the result set * @param string|null $sort SQL string for sorting * @return array */ function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) { global $CFG, $USER, $DB; if (empty($userid)) { $userid = $USER->id; } $basefields = [ 'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category', 'shortname', 'fullname', 'timeaccess', 'component', 'visible', 'showactivitydates', 'showcompletionconditions', 'pdfexportfont' ]; if (empty($sort)) { $sort = 'timeaccess DESC'; } else { // The SQL string for sorting can define sorting by multiple columns. $rawsorts = explode(',', $sort); $sorts = array(); // Validate and trim the sort parameters in the SQL string for sorting. foreach ($rawsorts as $rawsort) { $sort = trim($rawsort); $sortparams = explode(' ', $sort); // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess'). if (count($sortparams) > 2) { throw new invalid_parameter_exception( 'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].'); } $sortfield = trim($sortparams[0]); // Validate the value which defines the field to sort by. if (!in_array($sortfield, $basefields)) { throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' . implode(', ', $basefields) . '.'); } $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : ''; // Validate the value which defines the sort direction (if present). $allowedsortdirections = ['asc', 'desc']; if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) { throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' . implode(', ', $allowedsortdirections) . '.'); } $sorts[] = $sort; } $sort = implode(',', $sorts); } $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); $coursefields = 'c.' . join(',', $basefields); // Ask the favourites service to give us the join SQL for favourited courses, // so we can include favourite information in the query. $usercontext = \context_user::instance($userid); $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid'); $sql = "SELECT $coursefields, $ctxfields FROM {course} c JOIN {context} ctx ON ctx.contextlevel = :contextlevel AND ctx.instanceid = c.id JOIN {user_lastaccess} ul ON ul.courseid = c.id $favsql LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol WHERE ul.userid = :userid AND c.visible = :visible AND (eg.id IS NOT NULL OR EXISTS (SELECT e.id FROM {enrol} e JOIN {user_enrolments} ue ON ue.enrolid = e.id WHERE e.courseid = c.id AND e.status = :statusenrol AND ue.status = :status AND ue.userid = :userid2 AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2) )) ORDER BY $sort"; $now = round(time(), -2); // Improves db caching. $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE, 'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now, 'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams; $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit); // Filter courses if last access field is hidden. $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) { $recentcourses = array_filter($recentcourses, function($course) { context_helper::preload_from_record($course); $context = context_course::instance($course->id, IGNORE_MISSING); // If last access was a hidden field, a user requesting info about another user would need permission to view hidden // fields. return has_capability('moodle/course:viewhiddenuserfields', $context); }); } return $recentcourses; } /** * Calculate the course start date and offset for the given user ids. * * If the course is a fixed date course then the course start date will be returned. * If the course is a relative date course then the course date will be calculated and * and offset provided. * * The dates are returned as an array with the index being the user id. The array * contains the start date and start offset values for the user. * * If the user is not enrolled in the course then the course start date will be returned. * * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the * former is enrolled in the course at 1563244693 and the latter is not enrolled then the * return value would look like: * [ * '123' => [ * 'start' => 1563244693, * 'startoffset' => 693 * ], * '456' => [ * 'start' => 1563244000, * 'startoffset' => 0 * ] * ] * * @param stdClass $course The course to fetch dates for. * @param array $userids The list of user ids to get dates for. * @return array */ function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array { if (empty($course->relativedatesmode)) { // This course isn't set to relative dates so we can early return with the course // start date. return array_reduce($userids, function($carry, $userid) use ($course) { $carry[$userid] = [ 'start' => $course->startdate, 'startoffset' => 0 ]; return $carry; }, []); } // We're dealing with a relative dates course now so we need to calculate some dates. $cache = cache::make('core', 'course_user_dates'); $dates = []; $uncacheduserids = []; // Try fetching the values from the cache so that we don't need to do a DB request. foreach ($userids as $userid) { $cachekey = "{$course->id}_{$userid}"; $cachedvalue = $cache->get($cachekey); if ($cachedvalue === false) { // Looks like we haven't seen this user for this course before so we'll have // to fetch it. $uncacheduserids[] = $userid; } else { [$start, $startoffset] = $cachedvalue; $dates[$userid] = [ 'start' => $start, 'startoffset' => $startoffset ]; } } if (!empty($uncacheduserids)) { // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param // to false because it filters out users with enrolment start times in the future which // we don't want. $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids); foreach ($uncacheduserids as $userid) { // Find the user enrolment that has the earliest start date. $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) { // Only consider enrolments for this user if the user enrolment is active and the // enrolment method is enabled. if ( $enrolment->uestatus == ENROL_USER_ACTIVE && $enrolment->estatus == ENROL_INSTANCE_ENABLED && $enrolment->id == $userid ) { if (is_null($carry)) { // Haven't found an enrolment yet for this user so use the one we just found. $carry = $enrolment; } else { // We've already found an enrolment for this user so let's use which ever one // has the earliest start time. $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment; } } return $carry; }, null); if ($enrolment) { // The course is in relative dates mode so we calculate the student's start // date based on their enrolment start date. $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart; $startoffset = $start - $course->startdate; } else { // The user is not enrolled in the course so default back to the course start date. $start = $course->startdate; $startoffset = 0; } $dates[$userid] = [ 'start' => $start, 'startoffset' => $startoffset ]; $cachekey = "{$course->id}_{$userid}"; $cache->set($cachekey, [$start, $startoffset]); } } return $dates; } /** * Calculate the course start date and offset for the given user id. * * If the course is a fixed date course then the course start date will be returned. * If the course is a relative date course then the course date will be calculated and * and offset provided. * * The return array contains the start date and start offset values for the user. * * If the user is not enrolled in the course then the course start date will be returned. * * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693 * then the return would be: * [ * 'start' => 1563244693, * 'startoffset' => 693 * ] * * If the use was not enrolled then the return would be: * [ * 'start' => 1563244000, * 'startoffset' => 0 * ] * * @param stdClass $course The course to fetch dates for. * @param int $userid The user id to get dates for. * @return array */ function course_get_course_dates_for_user_id(stdClass $course, int $userid): array { return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid]; } /** * Renders the course copy form for the modal on the course management screen. * * @param array $args * @return string $o Form HTML. */ function course_output_fragment_new_base_form($args) { $serialiseddata = json_decode($args['jsonformdata'], true); $formdata = []; if (!empty($serialiseddata)) { parse_str($serialiseddata, $formdata); } $context = context_course::instance($args['courseid']); $copycaps = \core_course\management\helper::get_course_copy_capabilities(); require_all_capabilities($copycaps, $context); $course = get_course($args['courseid']); $mform = new \core_backup\output\copy_form( null, array('course' => $course, 'returnto' => '', 'returnurl' => ''), 'post', '', ['class' => 'ignoredirty'], true, $formdata); if (!empty($serialiseddata)) { // If we were passed non-empty form data we want the mform to call validation functions and show errors. $mform->is_validated(); } ob_start(); $mform->display(); $o = ob_get_contents(); ob_end_clean(); return $o; } /** * Get the current course image for the given course. * * @param \stdClass $course * @return null|stored_file */ function course_get_courseimage(\stdClass $course): ?stored_file { $courseinlist = new core_course_list_element($course); foreach ($courseinlist->get_course_overviewfiles() as $file) { if ($file->is_valid_image()) { return $file; } } return null; } /** * Get course specific data for configuring a communication instance. * * @param integer $courseid The course id. * @return array Returns course data, context and heading. */ function course_get_communication_instance_data(int $courseid): array { // Do some checks and prepare instance specific data. $course = get_course($courseid); require_login($course); $context = context_course::instance($course->id); require_capability('moodle/course:configurecoursecommunication', $context); $heading = $course->fullname; $returnurl = new moodle_url('/course/view.php', ['id' => $courseid]); return [$course, $context, $heading, $returnurl]; } /** * Update a course using communication configuration data. * * @param stdClass $data The data to update the course with. */ function course_update_communication_instance_data(stdClass $data): void { $data->id = $data->instanceid; // For correct use in update_course. update_course($data); } home/harasnat/www/learning/group/lib.php 0000604 00000133321 15062110235 0014325 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Extra library for groups and groupings. * * @copyright 2006 The Open University, J.White AT open.ac.uk, Petr Skoda (skodak) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_group */ /* * INTERNAL FUNCTIONS - to be used by moodle core only * require_once $CFG->dirroot.'/group/lib.php' must be used */ /** * Adds a specified user to a group * * @param mixed $grouporid The group id or group object * @param mixed $userorid The user id or user object * @param string $component Optional component name e.g. 'enrol_imsenterprise' * @param int $itemid Optional itemid associated with component * @return bool True if user added successfully or the user is already a * member of the group, false otherwise. */ function groups_add_member($grouporid, $userorid, $component=null, $itemid=0) { global $DB; if (is_object($userorid)) { $userid = $userorid->id; $user = $userorid; if (!isset($user->deleted)) { $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); } } else { $userid = $userorid; $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); } if ($user->deleted) { return false; } if (is_object($grouporid)) { $groupid = $grouporid->id; $group = $grouporid; } else { $groupid = $grouporid; $group = $DB->get_record('groups', array('id'=>$groupid), '*', MUST_EXIST); } // Check if the user a participant of the group course. $context = context_course::instance($group->courseid); if (!is_enrolled($context, $userid)) { return false; } if (groups_is_member($groupid, $userid)) { return true; } $member = new stdClass(); $member->groupid = $groupid; $member->userid = $userid; $member->timeadded = time(); $member->component = ''; $member->itemid = 0; // Check the component exists if specified if (!empty($component)) { $dir = core_component::get_component_directory($component); if ($dir && is_dir($dir)) { // Component exists and can be used $member->component = $component; $member->itemid = $itemid; } else { throw new coding_exception('Invalid call to groups_add_member(). An invalid component was specified'); } } if ($itemid !== 0 && empty($member->component)) { // An itemid can only be specified if a valid component was found throw new coding_exception('Invalid call to groups_add_member(). A component must be specified if an itemid is given'); } $DB->insert_record('groups_members', $member); // Update group info, and group object. $DB->set_field('groups', 'timemodified', $member->timeadded, array('id'=>$groupid)); $group->timemodified = $member->timeadded; // Invalidate the group and grouping cache for users. cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid)); // Group conversation messaging. if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) { \core_message\api::add_members_to_conversation([$userid], $conversation->id); } // Trigger group event. $params = array( 'context' => $context, 'objectid' => $groupid, 'relateduserid' => $userid, 'other' => array( 'component' => $member->component, 'itemid' => $member->itemid ) ); $event = \core\event\group_member_added::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); return true; } /** * Checks whether the current user is permitted (using the normal UI) to * remove a specific group member, assuming that they have access to remove * group members in general. * * For automatically-created group member entries, this checks with the * relevant plugin to see whether it is permitted. The default, if the plugin * doesn't provide a function, is true. * * For other entries (and any which have already been deleted/don't exist) it * just returns true. * * @param mixed $grouporid The group id or group object * @param mixed $userorid The user id or user object * @return bool True if permitted, false otherwise */ function groups_remove_member_allowed($grouporid, $userorid) { global $DB; if (is_object($userorid)) { $userid = $userorid->id; } else { $userid = $userorid; } if (is_object($grouporid)) { $groupid = $grouporid->id; } else { $groupid = $grouporid; } // Get entry if (!($entry = $DB->get_record('groups_members', array('groupid' => $groupid, 'userid' => $userid), '*', IGNORE_MISSING))) { // If the entry does not exist, they are allowed to remove it (this // is consistent with groups_remove_member below). return true; } // If the entry does not have a component value, they can remove it if (empty($entry->component)) { return true; } // It has a component value, so we need to call a plugin function (if it // exists); the default is to allow removal return component_callback($entry->component, 'allow_group_member_remove', array($entry->itemid, $entry->groupid, $entry->userid), true); } /** * Deletes the link between the specified user and group. * * @param mixed $grouporid The group id or group object * @param mixed $userorid The user id or user object * @return bool True if deletion was successful, false otherwise */ function groups_remove_member($grouporid, $userorid) { global $DB; if (is_object($userorid)) { $userid = $userorid->id; } else { $userid = $userorid; } if (is_object($grouporid)) { $groupid = $grouporid->id; $group = $grouporid; } else { $groupid = $grouporid; $group = $DB->get_record('groups', array('id'=>$groupid), '*', MUST_EXIST); } if (!groups_is_member($groupid, $userid)) { return true; } $DB->delete_records('groups_members', array('groupid'=>$groupid, 'userid'=>$userid)); // Update group info. $time = time(); $DB->set_field('groups', 'timemodified', $time, array('id' => $groupid)); $group->timemodified = $time; // Invalidate the group and grouping cache for users. cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid)); // Group conversation messaging. $context = context_course::instance($group->courseid); if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) { \core_message\api::remove_members_from_conversation([$userid], $conversation->id); } // Trigger group event. $params = array( 'context' => context_course::instance($group->courseid), 'objectid' => $groupid, 'relateduserid' => $userid ); $event = \core\event\group_member_removed::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); return true; } /** * Add a new group * * @param stdClass $data group properties * @param stdClass $editform * @param array $editoroptions * @return int id of group or throws an exception on error * @throws moodle_exception */ function groups_create_group($data, $editform = false, $editoroptions = false) { global $CFG, $DB, $USER; //check that courseid exists $course = $DB->get_record('course', array('id' => $data->courseid), '*', MUST_EXIST); $context = context_course::instance($course->id); $data->timecreated = time(); $data->timemodified = $data->timecreated; $data->name = trim($data->name); if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (groups_get_group_by_idnumber($course->id, $data->idnumber)) { throw new moodle_exception('idnumbertaken'); } } $data->visibility ??= GROUPS_VISIBILITY_ALL; if (!in_array($data->visibility, [GROUPS_VISIBILITY_ALL, GROUPS_VISIBILITY_MEMBERS])) { $data->participation = false; $data->enablemessaging = false; } if ($editform and $editoroptions) { $data->description = $data->description_editor['text']; $data->descriptionformat = $data->description_editor['format']; } $data->id = $DB->insert_record('groups', $data); $handler = \core_group\customfield\group_handler::create(); $handler->instance_form_save($data, true); if ($editform and $editoroptions) { // Update description from editor with fixed files $data = file_postupdate_standard_editor($data, 'description', $editoroptions, $context, 'group', 'description', $data->id); $upd = new stdClass(); $upd->id = $data->id; $upd->description = $data->description; $upd->descriptionformat = $data->descriptionformat; $DB->update_record('groups', $upd); } $group = $DB->get_record('groups', array('id'=>$data->id)); if ($editform) { groups_update_group_icon($group, $data, $editform); } // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($course->id)); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($course->id); // Group conversation messaging. if (\core_message\api::can_create_group_conversation($USER->id, $context)) { if (!empty($data->enablemessaging)) { \core_message\api::create_conversation( \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, [], $group->name, \core_message\api::MESSAGE_CONVERSATION_ENABLED, 'core_group', 'groups', $group->id, $context->id); } } // Trigger group event. $params = array( 'context' => $context, 'objectid' => $group->id ); $event = \core\event\group_created::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); return $group->id; } /** * Add a new grouping * * @param stdClass $data grouping properties * @param array $editoroptions * @return int id of grouping or throws an exception on error * @throws moodle_exception */ function groups_create_grouping($data, $editoroptions=null) { global $DB; $data->timecreated = time(); $data->timemodified = $data->timecreated; $data->name = trim($data->name); if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) { throw new moodle_exception('idnumbertaken'); } } if ($editoroptions !== null) { $data->description = $data->description_editor['text']; $data->descriptionformat = $data->description_editor['format']; } $id = $DB->insert_record('groupings', $data); $data->id = $id; $handler = \core_group\customfield\grouping_handler::create(); $handler->instance_form_save($data, true); if ($editoroptions !== null) { $description = new stdClass; $description->id = $data->id; $description->description_editor = $data->description_editor; $description = file_postupdate_standard_editor($description, 'description', $editoroptions, $editoroptions['context'], 'grouping', 'description', $description->id); $DB->update_record('groupings', $description); } // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); // Trigger group event. $params = array( 'context' => context_course::instance($data->courseid), 'objectid' => $id ); $event = \core\event\grouping_created::create($params); $event->trigger(); return $id; } /** * Update the group icon from form data * * @param stdClass $group group information * @param stdClass $data * @param stdClass $editform */ function groups_update_group_icon($group, $data, $editform) { global $CFG, $DB; require_once("$CFG->libdir/gdlib.php"); $fs = get_file_storage(); $context = context_course::instance($group->courseid, MUST_EXIST); $newpicture = $group->picture; if (!empty($data->deletepicture)) { $fs->delete_area_files($context->id, 'group', 'icon', $group->id); $newpicture = 0; } else if ($iconfile = $editform->save_temp_file('imagefile')) { if ($rev = process_new_icon($context, 'group', 'icon', $group->id, $iconfile)) { $newpicture = $rev; } else { $fs->delete_area_files($context->id, 'group', 'icon', $group->id); $newpicture = 0; } @unlink($iconfile); } if ($newpicture != $group->picture) { $DB->set_field('groups', 'picture', $newpicture, array('id' => $group->id)); $group->picture = $newpicture; // Invalidate the group data as we've updated the group record. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($group->courseid)); } } /** * Update group * * @param stdClass $data group properties (with magic quotes) * @param stdClass $editform * @param array $editoroptions * @return bool true or exception */ function groups_update_group($data, $editform = false, $editoroptions = false) { global $CFG, $DB, $USER; $context = context_course::instance($data->courseid); $data->timemodified = time(); if (isset($data->name)) { $data->name = trim($data->name); } if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (($existing = groups_get_group_by_idnumber($data->courseid, $data->idnumber)) && $existing->id != $data->id) { throw new moodle_exception('idnumbertaken'); } } if (isset($data->visibility) && !in_array($data->visibility, [GROUPS_VISIBILITY_ALL, GROUPS_VISIBILITY_MEMBERS])) { $data->participation = false; $data->enablemessaging = false; } if ($editform and $editoroptions) { $data = file_postupdate_standard_editor($data, 'description', $editoroptions, $context, 'group', 'description', $data->id); } $DB->update_record('groups', $data); $handler = \core_group\customfield\group_handler::create(); $handler->instance_form_save($data); // Invalidate the group data. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($data->courseid); $group = $DB->get_record('groups', array('id'=>$data->id)); if ($editform) { groups_update_group_icon($group, $data, $editform); } // Group conversation messaging. if (\core_message\api::can_create_group_conversation($USER->id, $context)) { if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $group->id, $context->id)) { if ($data->enablemessaging && $data->enablemessaging != $conversation->enabled) { \core_message\api::enable_conversation($conversation->id); } if (!$data->enablemessaging && $data->enablemessaging != $conversation->enabled) { \core_message\api::disable_conversation($conversation->id); } \core_message\api::update_conversation_name($conversation->id, $group->name); } else { if (!empty($data->enablemessaging)) { $conversation = \core_message\api::create_conversation( \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, [], $group->name, \core_message\api::MESSAGE_CONVERSATION_ENABLED, 'core_group', 'groups', $group->id, $context->id ); // Add members to conversation if they exists in the group. if ($groupmemberroles = groups_get_members_by_role($group->id, $group->courseid, 'u.id')) { $users = []; foreach ($groupmemberroles as $roleid => $roledata) { foreach ($roledata->users as $member) { $users[] = $member->id; } } \core_message\api::add_members_to_conversation($users, $conversation->id); } } } } // Trigger group event. $params = array( 'context' => $context, 'objectid' => $group->id ); $event = \core\event\group_updated::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); return true; } /** * Update grouping * * @param stdClass $data grouping properties (with magic quotes) * @param array $editoroptions * @return bool true or exception */ function groups_update_grouping($data, $editoroptions=null) { global $DB; $data->timemodified = time(); if (isset($data->name)) { $data->name = trim($data->name); } if (isset($data->idnumber)) { $data->idnumber = trim($data->idnumber); if (($existing = groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) && $existing->id != $data->id) { throw new moodle_exception('idnumbertaken'); } } if ($editoroptions !== null) { $data = file_postupdate_standard_editor($data, 'description', $editoroptions, $editoroptions['context'], 'grouping', 'description', $data->id); } $DB->update_record('groupings', $data); $handler = \core_group\customfield\grouping_handler::create(); $handler->instance_form_save($data); // Invalidate the group data. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); // Trigger group event. $params = array( 'context' => context_course::instance($data->courseid), 'objectid' => $data->id ); $event = \core\event\grouping_updated::create($params); $event->trigger(); return true; } /** * Delete a group best effort, first removing members and links with courses and groupings. * Removes group avatar too. * * @param mixed $grouporid The id of group to delete or full group object * @return bool True if deletion was successful, false otherwise */ function groups_delete_group($grouporid) { global $CFG, $DB; require_once("$CFG->libdir/gdlib.php"); if (is_object($grouporid)) { $groupid = $grouporid->id; $group = $grouporid; } else { $groupid = $grouporid; if (!$group = $DB->get_record('groups', array('id'=>$groupid))) { //silently ignore attempts to delete missing already deleted groups ;-) return true; } } $context = context_course::instance($group->courseid); // delete group calendar events $DB->delete_records('event', array('groupid'=>$groupid)); //first delete usage in groupings_groups $DB->delete_records('groupings_groups', array('groupid'=>$groupid)); //delete members $DB->delete_records('groups_members', array('groupid'=>$groupid)); // Delete any members in a conversation related to this group. if ($conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $groupid, $context->id)) { \core_message\api::delete_all_conversation_data($conversation->id); } //group itself last $DB->delete_records('groups', array('id'=>$groupid)); // Delete all files associated with this group $context = context_course::instance($group->courseid); $fs = get_file_storage(); $fs->delete_area_files($context->id, 'group', 'description', $groupid); $fs->delete_area_files($context->id, 'group', 'icon', $groupid); // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($group->courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($group->courseid); // Trigger group event. $params = array( 'context' => $context, 'objectid' => $groupid ); $event = \core\event\group_deleted::create($params); $event->add_record_snapshot('groups', $group); $event->trigger(); return true; } /** * Delete grouping * * @param int $groupingorid * @return bool success */ function groups_delete_grouping($groupingorid) { global $DB; if (is_object($groupingorid)) { $groupingid = $groupingorid->id; $grouping = $groupingorid; } else { $groupingid = $groupingorid; if (!$grouping = $DB->get_record('groupings', array('id'=>$groupingorid))) { //silently ignore attempts to delete missing already deleted groupings ;-) return true; } } //first delete usage in groupings_groups $DB->delete_records('groupings_groups', array('groupingid'=>$groupingid)); // remove the default groupingid from course $DB->set_field('course', 'defaultgroupingid', 0, array('defaultgroupingid'=>$groupingid)); // remove the groupingid from all course modules $DB->set_field('course_modules', 'groupingid', 0, array('groupingid'=>$groupingid)); //group itself last $DB->delete_records('groupings', array('id'=>$groupingid)); $context = context_course::instance($grouping->courseid); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'grouping', 'description', $groupingid); foreach ($files as $file) { $file->delete(); } // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($grouping->courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // Trigger group event. $params = array( 'context' => $context, 'objectid' => $groupingid ); $event = \core\event\grouping_deleted::create($params); $event->add_record_snapshot('groupings', $grouping); $event->trigger(); return true; } /** * Remove all users (or one user) from all groups in course * * @param int $courseid * @param int $userid 0 means all users * @param bool $unused - formerly $showfeedback, is no longer used. * @return bool success */ function groups_delete_group_members($courseid, $userid=0, $unused=false) { global $DB, $OUTPUT; // Get the users in the course which are in a group. $sql = "SELECT gm.id as gmid, gm.userid, g.* FROM {groups_members} gm INNER JOIN {groups} g ON gm.groupid = g.id WHERE g.courseid = :courseid"; $params = array(); $params['courseid'] = $courseid; // Check if we want to delete a specific user. if ($userid) { $sql .= " AND gm.userid = :userid"; $params['userid'] = $userid; } $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $usergroup) { groups_remove_member($usergroup, $usergroup->userid); } $rs->close(); return true; } /** * Remove all groups from all groupings in course * * @param int $courseid * @param bool $showfeedback * @return bool success */ function groups_delete_groupings_groups($courseid, $showfeedback=false) { global $DB, $OUTPUT; $groupssql = "SELECT id FROM {groups} g WHERE g.courseid = ?"; $results = $DB->get_recordset_select('groupings_groups', "groupid IN ($groupssql)", array($courseid), '', 'groupid, groupingid'); foreach ($results as $result) { groups_unassign_grouping($result->groupingid, $result->groupid, false); } $results->close(); // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // no need to show any feedback here - we delete usually first groupings and then groups return true; } /** * Delete all groups from course * * @param int $courseid * @param bool $showfeedback * @return bool success */ function groups_delete_groups($courseid, $showfeedback=false) { global $CFG, $DB, $OUTPUT; $groups = $DB->get_recordset('groups', array('courseid' => $courseid)); foreach ($groups as $group) { groups_delete_group($group); } $groups->close(); // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); // Rebuild the coursehiddengroups cache for the course. \core_group\visibility::update_hiddengroups_cache($courseid); if ($showfeedback) { echo $OUTPUT->notification(get_string('deleted').' - '.get_string('groups', 'group'), 'notifysuccess'); } return true; } /** * Delete all groupings from course * * @param int $courseid * @param bool $showfeedback * @return bool success */ function groups_delete_groupings($courseid, $showfeedback=false) { global $DB, $OUTPUT; $groupings = $DB->get_recordset_select('groupings', 'courseid = ?', array($courseid)); foreach ($groupings as $grouping) { groups_delete_grouping($grouping); } $groupings->close(); // Invalidate the grouping cache for the course. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); if ($showfeedback) { echo $OUTPUT->notification(get_string('deleted').' - '.get_string('groupings', 'group'), 'notifysuccess'); } return true; } /* =================================== */ /* various functions used by groups UI */ /* =================================== */ /** * Obtains a list of the possible roles that group members might come from, * on a course. Generally this includes only profile roles. * * @param context $context Context of course * @return Array of role ID integers, or false if error/none. */ function groups_get_possible_roles($context) { $roles = get_profile_roles($context); return array_keys($roles); } /** * Gets potential group members for grouping * * @param int $courseid The id of the course * @param int $roleid The role to select users from * @param mixed $source restrict to cohort, grouping or group id * @param string $orderby The column to sort users by * @param int $notingroup restrict to users not in existing groups * @param bool $onlyactiveenrolments restrict to users who have an active enrolment in the course * @param array $extrafields Extra user fields to return * @return array An array of the users */ function groups_get_potential_members($courseid, $roleid = null, $source = null, $orderby = 'lastname ASC, firstname ASC', $notingroup = null, $onlyactiveenrolments = false, $extrafields = []) { global $DB; $context = context_course::instance($courseid); list($esql, $params) = get_enrolled_sql($context, '', 0, $onlyactiveenrolments); $notingroupsql = ""; if ($notingroup) { // We want to eliminate users that are already associated with a course group. $notingroupsql = "u.id NOT IN (SELECT userid FROM {groups_members} WHERE groupid IN (SELECT id FROM {groups} WHERE courseid = :courseid))"; $params['courseid'] = $courseid; } if ($roleid) { // We are looking for all users with this role assigned in this context or higher. list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); $params = array_merge($params, $relatedctxparams, array('roleid' => $roleid)); $where = "WHERE u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)"; $where .= $notingroup ? "AND $notingroupsql" : ""; } else if ($notingroup) { $where = "WHERE $notingroupsql"; } else { $where = ""; } $sourcejoin = ""; if (is_int($source)) { $sourcejoin .= "JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid) "; $params['cohortid'] = $source; } else { // Auto-create groups from an existing cohort membership. if (isset($source['cohortid'])) { $sourcejoin .= "JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid) "; $params['cohortid'] = $source['cohortid']; } // Auto-create groups from an existing group membership. if (isset($source['groupid'])) { $sourcejoin .= "JOIN {groups_members} gp ON (gp.userid = u.id AND gp.groupid = :groupid) "; $params['groupid'] = $source['groupid']; } // Auto-create groups from an existing grouping membership. if (isset($source['groupingid'])) { $sourcejoin .= "JOIN {groupings_groups} gg ON gg.groupingid = :groupingid "; $sourcejoin .= "JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = gg.groupid) "; $params['groupingid'] = $source['groupingid']; } } $userfieldsapi = \core_user\fields::for_userpic()->including(...$extrafields); $allusernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT DISTINCT u.id, u.username, $allusernamefields, u.idnumber FROM {user} u JOIN ($esql) e ON e.id = u.id $sourcejoin $where ORDER BY $orderby"; return $DB->get_records_sql($sql, $params); } /** * Parse a group name for characters to replace * * @param string $format The format a group name will follow * @param int $groupnumber The number of the group to be used in the parsed format string * @return string the parsed format string */ function groups_parse_name($format, $groupnumber) { if (strstr($format, '@') !== false) { // Convert $groupnumber to a character series $letter = 'A'; for($i=0; $i<$groupnumber; $i++) { $letter++; } $str = str_replace('@', $letter, $format); } else { $str = str_replace('#', $groupnumber+1, $format); } return($str); } /** * Assigns group into grouping * * @param int groupingid * @param int groupid * @param int $timeadded The time the group was added to the grouping. * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well. * @return bool true or exception */ function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $invalidatecache = true) { global $DB; if ($DB->record_exists('groupings_groups', array('groupingid'=>$groupingid, 'groupid'=>$groupid))) { return true; } $assign = new stdClass(); $assign->groupingid = $groupingid; $assign->groupid = $groupid; if ($timeadded != null) { $assign->timeadded = (integer)$timeadded; } else { $assign->timeadded = time(); } $DB->insert_record('groupings_groups', $assign); $courseid = $DB->get_field('groupings', 'courseid', array('id' => $groupingid)); if ($invalidatecache) { // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); } // Trigger event. $params = array( 'context' => context_course::instance($courseid), 'objectid' => $groupingid, 'other' => array('groupid' => $groupid) ); $event = \core\event\grouping_group_assigned::create($params); $event->trigger(); return true; } /** * Unassigns group from grouping * * @param int groupingid * @param int groupid * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well. * @return bool success */ function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true) { global $DB; $DB->delete_records('groupings_groups', array('groupingid'=>$groupingid, 'groupid'=>$groupid)); $courseid = $DB->get_field('groupings', 'courseid', array('id' => $groupingid)); if ($invalidatecache) { // Invalidate the grouping cache for the course cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); // Purge the group and grouping cache for users. cache_helper::purge_by_definition('core', 'user_group_groupings'); } // Trigger event. $params = array( 'context' => context_course::instance($courseid), 'objectid' => $groupingid, 'other' => array('groupid' => $groupid) ); $event = \core\event\grouping_group_unassigned::create($params); $event->trigger(); return true; } /** * Lists users in a group based on their role on the course. * Returns false if there's an error or there are no users in the group. * Otherwise returns an array of role ID => role data, where role data includes: * (role) $id, $shortname, $name * $users: array of objects for each user which include the specified fields * Users who do not have a role are stored in the returned array with key '-' * and pseudo-role details (including a name, 'No role'). Users with multiple * roles, same deal with key '*' and name 'Multiple roles'. You can find out * which roles each has by looking in the $roles array of the user object. * * @param int $groupid * @param int $courseid Course ID (should match the group's course) * @param string $fields List of fields from user table (prefixed with u) and joined tables, default 'u.*' * @param string|null $sort SQL ORDER BY clause, default (when null passed) is what comes from users_order_by_sql. * @param string $extrawheretest extra SQL conditions ANDed with the existing where clause. * @param array $whereorsortparams any parameters required by $extrawheretest or $joins (named parameters). * @param string $joins any joins required to get the specified fields. * @return array Complex array as described above */ function groups_get_members_by_role(int $groupid, int $courseid, string $fields = 'u.*', ?string $sort = null, string $extrawheretest = '', array $whereorsortparams = [], string $joins = '') { global $DB; // Retrieve information about all users and their roles on the course or // parent ('related') contexts $context = context_course::instance($courseid); // We are looking for all users with this role assigned in this context or higher. list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); if ($extrawheretest) { $extrawheretest = ' AND ' . $extrawheretest; } if (is_null($sort)) { list($sort, $sortparams) = users_order_by_sql('u'); $whereorsortparams = array_merge($whereorsortparams, $sortparams); } $sql = "SELECT r.id AS roleid, u.id AS userid, $fields FROM {groups_members} gm JOIN {user} u ON u.id = gm.userid LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql) LEFT JOIN {role} r ON r.id = ra.roleid $joins WHERE gm.groupid=:mgroupid ".$extrawheretest." ORDER BY r.sortorder, $sort"; $whereorsortparams = array_merge($whereorsortparams, $relatedctxparams, array('mgroupid' => $groupid)); $rs = $DB->get_recordset_sql($sql, $whereorsortparams); return groups_calculate_role_people($rs, $context); } /** * Internal function used by groups_get_members_by_role to handle the * results of a database query that includes a list of users and possible * roles on a course. * * @param moodle_recordset $rs The record set (may be false) * @param int $context ID of course context * @return array As described in groups_get_members_by_role */ function groups_calculate_role_people($rs, $context) { global $CFG, $DB; if (!$rs) { return array(); } $allroles = role_fix_names(get_all_roles($context), $context); $visibleroles = get_viewable_roles($context); // Array of all involved roles $roles = array(); // Array of all retrieved users $users = array(); // Fill arrays foreach ($rs as $rec) { // Create information about user if this is a new one if (!array_key_exists($rec->userid, $users)) { // User data includes all the optional fields, but not any of the // stuff we added to get the role details $userdata = clone($rec); unset($userdata->roleid); unset($userdata->roleshortname); unset($userdata->rolename); unset($userdata->userid); $userdata->id = $rec->userid; // Make an array to hold the list of roles for this user $userdata->roles = array(); $users[$rec->userid] = $userdata; } // If user has a role... if (!is_null($rec->roleid)) { // Create information about role if this is a new one if (!array_key_exists($rec->roleid, $roles)) { $role = $allroles[$rec->roleid]; $roledata = new stdClass(); $roledata->id = $role->id; $roledata->shortname = $role->shortname; $roledata->name = $role->localname; $roledata->users = array(); $roles[$roledata->id] = $roledata; } // Record that user has role $users[$rec->userid]->roles[$rec->roleid] = $roles[$rec->roleid]; } } $rs->close(); // Return false if there weren't any users if (count($users) == 0) { return false; } // Add pseudo-role for multiple roles $roledata = new stdClass(); $roledata->name = get_string('multipleroles','role'); $roledata->users = array(); $roles['*'] = $roledata; $roledata = new stdClass(); $roledata->name = get_string('noroles','role'); $roledata->users = array(); $roles[0] = $roledata; // Now we rearrange the data to store users by role foreach ($users as $userid=>$userdata) { $visibleuserroles = array_intersect_key($userdata->roles, $visibleroles); $rolecount = count($visibleuserroles); if ($rolecount == 0) { // does not have any roles $roleid = 0; } else if($rolecount > 1) { $roleid = '*'; } else { $userrole = reset($visibleuserroles); $roleid = $userrole->id; } $roles[$roleid]->users[$userid] = $userdata; } // Delete roles not used foreach ($roles as $key=>$roledata) { if (count($roledata->users)===0) { unset($roles[$key]); } } // Return list of roles containing their users return $roles; } /** * Synchronises enrolments with the group membership * * Designed for enrolment methods provide automatic synchronisation between enrolled users * and group membership, such as enrol_cohort and enrol_meta . * * @param string $enrolname name of enrolment method without prefix * @param int $courseid course id where sync needs to be performed (0 for all courses) * @param string $gidfield name of the field in 'enrol' table that stores group id * @return array Returns the list of removed and added users. Each record contains fields: * userid, enrolid, courseid, groupid, groupname */ function groups_sync_with_enrolment($enrolname, $courseid = 0, $gidfield = 'customint2') { global $DB; $onecourse = $courseid ? "AND e.courseid = :courseid" : ""; $params = array( 'enrolname' => $enrolname, 'component' => 'enrol_'.$enrolname, 'courseid' => $courseid ); $affectedusers = array( 'removed' => array(), 'added' => array() ); // Remove invalid. $sql = "SELECT ue.userid, ue.enrolid, e.courseid, g.id AS groupid, g.name AS groupname FROM {groups_members} gm JOIN {groups} g ON (g.id = gm.groupid) JOIN {enrol} e ON (e.enrol = :enrolname AND e.courseid = g.courseid $onecourse) JOIN {user_enrolments} ue ON (ue.userid = gm.userid AND ue.enrolid = e.id) WHERE gm.component=:component AND gm.itemid = e.id AND g.id <> e.{$gidfield}"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $gm) { groups_remove_member($gm->groupid, $gm->userid); $affectedusers['removed'][] = $gm; } $rs->close(); // Add missing. $sql = "SELECT ue.userid, ue.enrolid, e.courseid, g.id AS groupid, g.name AS groupname FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrolname $onecourse) JOIN {groups} g ON (g.courseid = e.courseid AND g.id = e.{$gidfield}) JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0) LEFT JOIN {groups_members} gm ON (gm.groupid = g.id AND gm.userid = ue.userid) WHERE gm.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ue) { groups_add_member($ue->groupid, $ue->userid, 'enrol_'.$enrolname, $ue->enrolid); $affectedusers['added'][] = $ue; } $rs->close(); return $affectedusers; } /** * Callback for inplace editable API. * * @param string $itemtype - Only user_groups is supported. * @param string $itemid - Userid and groupid separated by a : * @param string $newvalue - json encoded list of groupids. * @return \core\output\inplace_editable */ function core_group_inplace_editable($itemtype, $itemid, $newvalue) { if ($itemtype === 'user_groups') { return \core_group\output\user_groups_editable::update($itemid, $newvalue); } } /** * Updates group messaging to enable/disable in bulk. * * @param array $groupids array of group id numbers. * @param bool $enabled if true, enables messaging else disables messaging */ function set_groups_messaging(array $groupids, bool $enabled): void { foreach ($groupids as $groupid) { $data = groups_get_group($groupid, '*', MUST_EXIST); $data->enablemessaging = $enabled; groups_update_group($data); } } /** * Returns custom fields data for provided groups. * * @param array $groupids a list of group IDs to provide data for. * @return \core_customfield\data_controller[] */ function get_group_custom_fields_data(array $groupids): array { $result = []; if (!empty($groupids)) { $handler = \core_group\customfield\group_handler::create(); $customfieldsdata = $handler->get_instances_data($groupids, true); foreach ($customfieldsdata as $groupid => $fieldcontrollers) { foreach ($fieldcontrollers as $fieldcontroller) { $result[$groupid][] = [ 'type' => $fieldcontroller->get_field()->get('type'), 'value' => $fieldcontroller->export_value(), 'valueraw' => $fieldcontroller->get_value(), 'name' => $fieldcontroller->get_field()->get('name'), 'shortname' => $fieldcontroller->get_field()->get('shortname'), ]; } } } return $result; } /** * Returns custom fields data for provided groupings. * * @param array $groupingids a list of group IDs to provide data for. * @return \core_customfield\data_controller[] */ function get_grouping_custom_fields_data(array $groupingids): array { $result = []; if (!empty($groupingids)) { $handler = \core_group\customfield\grouping_handler::create(); $customfieldsdata = $handler->get_instances_data($groupingids, true); foreach ($customfieldsdata as $groupingid => $fieldcontrollers) { foreach ($fieldcontrollers as $fieldcontroller) { $result[$groupingid][] = [ 'type' => $fieldcontroller->get_field()->get('type'), 'value' => $fieldcontroller->export_value(), 'valueraw' => $fieldcontroller->get_value(), 'name' => $fieldcontroller->get_field()->get('name'), 'shortname' => $fieldcontroller->get_field()->get('shortname'), ]; } } } return $result; } home/harasnat/www/learning/admin/roles/lib.php 0000604 00000004245 15062122771 0015417 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Library code used by the roles administration interfaces. * * @package core_role * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Get the potential assignees selector for a given context. * * If this context is a course context, or inside a course context (module or * some blocks) then return a core_role_potential_assignees_below_course object. Otherwise * return a core_role_potential_assignees_course_and_above. * * @param context $context a context. * @param string $name passed to user selector constructor. * @param array $options to user selector constructor. * @return user_selector_base an appropriate user selector. */ function core_role_get_potential_user_selector(context $context, $name, $options) { $blockinsidecourse = false; if ($context->contextlevel == CONTEXT_BLOCK) { $parentcontext = $context->get_parent_context(); $blockinsidecourse = in_array($parentcontext->contextlevel, array(CONTEXT_MODULE, CONTEXT_COURSE)); } if (($context->contextlevel == CONTEXT_MODULE || $blockinsidecourse) && !is_inside_frontpage($context)) { $potentialuserselector = new core_role_potential_assignees_below_course('addselect', $options); } else { $potentialuserselector = new core_role_potential_assignees_course_and_above('addselect', $options); } return $potentialuserselector; } home/harasnat/www/learning/enrol/meta/lib.php 0000604 00000042330 15062124021 0015233 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Meta course enrolment plugin. * * @package enrol_meta * @copyright 2010 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * ENROL_META_CREATE_GROUP constant for automatically creating a group for a meta course. */ define('ENROL_META_CREATE_GROUP', -1); /** * Meta course enrolment plugin. * @author Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class enrol_meta_plugin extends enrol_plugin { /** * Returns localised name of enrol instance * * @param stdClass $instance (null is accepted too) * @return string */ public function get_instance_name($instance) { global $DB; if (empty($instance)) { $enrol = $this->get_name(); return get_string('pluginname', 'enrol_'.$enrol); } else if (empty($instance->name)) { $enrol = $this->get_name(); $course = $DB->get_record('course', array('id'=>$instance->customint1)); if ($course) { $coursename = format_string(get_course_display_name_for_list($course)); } else { // Use course id, if course is deleted. $coursename = $instance->customint1; } return get_string('pluginname', 'enrol_' . $enrol) . ' (' . $coursename . ')'; } else { return format_string($instance->name); } } /** * Returns true if we can add a new instance to this course. * * @param int $courseid * @return boolean */ public function can_add_instance($courseid) { $context = context_course::instance($courseid, MUST_EXIST); if (!has_capability('moodle/course:enrolconfig', $context) or !has_capability('enrol/meta:config', $context)) { return false; } // Multiple instances supported - multiple parent courses linked. return true; } /** * Does this plugin allow manual unenrolment of a specific user? * Yes, but only if user suspended... * * @param stdClass $instance course enrol instance * @param stdClass $ue record from user_enrolments table * * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment */ public function allow_unenrol_user(stdClass $instance, stdClass $ue) { if ($ue->status == ENROL_USER_SUSPENDED) { return true; } return false; } /** * Called after updating/inserting course. * * @param bool $inserted true if course just inserted * @param stdClass $course * @param stdClass $data form data * @return void */ public function course_updated($inserted, $course, $data) { // Meta sync updates are slow, if enrolments get out of sync teacher will have to wait till next cron. // We should probably add some sync button to the course enrol methods overview page. } /** * Add new instance of enrol plugin. * @param object $course * @param array $fields instance fields * @return int id of last instance, null if can not be created */ public function add_instance($course, array $fields = null) { global $CFG; require_once("$CFG->dirroot/enrol/meta/locallib.php"); // Support creating multiple at once. if (isset($fields['customint1']) && is_array($fields['customint1'])) { $courses = array_unique($fields['customint1']); } else if (isset($fields['customint1'])) { $courses = array($fields['customint1']); } else { $courses = array(null); // Strange? Yes, but that's how it's working or instance is not created ever. } foreach ($courses as $courseid) { if (!empty($fields['customint2']) && $fields['customint2'] == ENROL_META_CREATE_GROUP) { $context = context_course::instance($course->id); require_capability('moodle/course:managegroups', $context); $groupid = enrol_meta_create_new_group($course->id, $courseid); $fields['customint2'] = $groupid; } $fields['customint1'] = $courseid; $result = parent::add_instance($course, $fields); } enrol_meta_sync($course->id); return $result; } /** * Update instance of enrol plugin. * @param stdClass $instance * @param stdClass $data modified instance fields * @return boolean */ public function update_instance($instance, $data) { global $CFG; require_once("$CFG->dirroot/enrol/meta/locallib.php"); if (!empty($data->customint2) && $data->customint2 == ENROL_META_CREATE_GROUP) { $context = context_course::instance($instance->courseid); require_capability('moodle/course:managegroups', $context); $groupid = enrol_meta_create_new_group($instance->courseid, $data->customint1); $data->customint2 = $groupid; } $result = parent::update_instance($instance, $data); enrol_meta_sync($instance->courseid); return $result; } /** * Update instance status * * @param stdClass $instance * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED * @return void */ public function update_status($instance, $newstatus) { global $CFG; parent::update_status($instance, $newstatus); require_once("$CFG->dirroot/enrol/meta/locallib.php"); enrol_meta_sync($instance->courseid); } /** * Is it possible to delete enrol instance via standard UI? * * @param stdClass $instance * @return bool */ public function can_delete_instance($instance) { $context = context_course::instance($instance->courseid); return has_capability('enrol/meta:config', $context); } /** * Is it possible to hide/show enrol instance via standard UI? * * @param stdClass $instance * @return bool */ public function can_hide_show_instance($instance) { $context = context_course::instance($instance->courseid); return has_capability('enrol/meta:config', $context); } /** * We are a good plugin and don't invent our own UI/validation code path. * * @return boolean */ public function use_standard_editing_ui() { return true; } /** * Return an array of valid options for the courses. * * @param stdClass $instance * @param context $coursecontext * @return array */ protected function get_course_options($instance, $coursecontext) { global $DB; if ($instance->id) { $where = 'WHERE c.id = :courseid'; $params = array('courseid' => $instance->customint1); $existing = array(); } else { $where = ''; $params = array(); $instanceparams = array('enrol' => 'meta', 'courseid' => $instance->courseid); $existing = $DB->get_records('enrol', $instanceparams, '', 'customint1, id'); } // TODO: this has to be done via ajax or else it will fail very badly on large sites! $courses = array(); $select = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $sortorder = 'c.' . $this->get_config('coursesort', 'sortorder') . ' ASC'; $sql = "SELECT c.id, c.fullname, c.shortname, c.visible $select FROM {course} c $join $where ORDER BY $sortorder"; $rs = $DB->get_recordset_sql($sql, array('contextlevel' => CONTEXT_COURSE) + $params); foreach ($rs as $c) { if ($c->id == SITEID or $c->id == $instance->courseid or isset($existing[$c->id])) { continue; } context_helper::preload_from_record($c); $coursecontext = context_course::instance($c->id); if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { continue; } if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) { continue; } $courses[$c->id] = $coursecontext->get_context_name(false); } $rs->close(); return $courses; } /** * Return an array of valid options for the groups. * * @param context $coursecontext * @return array */ protected function get_group_options($coursecontext) { $groups = array(0 => get_string('none')); $courseid = $coursecontext->instanceid; if (has_capability('moodle/course:managegroups', $coursecontext)) { $groups[ENROL_META_CREATE_GROUP] = get_string('creategroup', 'enrol_meta'); } foreach (groups_get_all_groups($courseid) as $group) { $groups[$group->id] = format_string($group->name, true, array('context' => $coursecontext)); } return $groups; } /** * Add elements to the edit instance form. * * @param stdClass $instance * @param MoodleQuickForm $mform * @param context $coursecontext * @return bool */ public function edit_instance_form($instance, MoodleQuickForm $mform, $coursecontext) { global $DB; $groups = $this->get_group_options($coursecontext); $existing = $DB->get_records('enrol', array('enrol' => 'meta', 'courseid' => $coursecontext->instanceid), '', 'customint1, id'); $excludelist = array($coursecontext->instanceid); foreach ($existing as $existinginstance) { $excludelist[] = $existinginstance->customint1; } $options = array( 'requiredcapabilities' => array('enrol/meta:selectaslinked'), 'multiple' => empty($instance->id), // We only accept multiple values on creation. 'exclude' => $excludelist ); $mform->addElement('course', 'customint1', get_string('linkedcourse', 'enrol_meta'), $options); $mform->addRule('customint1', get_string('required'), 'required', null, 'client'); if (!empty($instance->id)) { $mform->freeze('customint1'); } $mform->addElement('select', 'customint2', get_string('addgroup', 'enrol_meta'), $groups); } /** * Perform custom validation of the data used to edit the instance. * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @param object $instance The instance loaded from the DB * @param context $context The context of the instance we are editing * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK. * @return void */ public function edit_instance_validation($data, $files, $instance, $context) { global $DB; $errors = array(); $thiscourseid = $context->instanceid; if (!empty($data['customint1'])) { $coursesidarr = is_array($data['customint1']) ? $data['customint1'] : [$data['customint1']]; list($coursesinsql, $coursesinparams) = $DB->get_in_or_equal($coursesidarr, SQL_PARAMS_NAMED, 'metacourseid'); if ($coursesrecords = $DB->get_records_select('course', "id {$coursesinsql}", $coursesinparams, '', 'id,visible')) { // Cast NULL to 0 to avoid possible mess with the SQL. $instanceid = $instance->id ?? 0; $existssql = "enrol = :meta AND courseid = :currentcourseid AND id != :id AND customint1 {$coursesinsql}"; $existsparams = [ 'meta' => 'meta', 'currentcourseid' => $thiscourseid, 'id' => $instanceid ]; $existsparams += $coursesinparams; if ($DB->record_exists_select('enrol', $existssql, $existsparams)) { // We may leave right here as further checks do not make sense in case we have existing enrol records // with the parameters from above. $errors['customint1'] = get_string('invalidcourseid', 'error'); } else { foreach ($coursesrecords as $coursesrecord) { $coursecontext = context_course::instance($coursesrecord->id); if (!$coursesrecord->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { $errors['customint1'] = get_string('nopermissions', 'error', 'moodle/course:viewhiddencourses'); } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) { $errors['customint1'] = get_string('nopermissions', 'error', 'enrol/meta:selectaslinked'); } else if ($coursesrecord->id == SITEID or $coursesrecord->id == $thiscourseid) { $errors['customint1'] = get_string('invalidcourseid', 'error'); } } } } else { $errors['customint1'] = get_string('invalidcourseid', 'error'); } } else { $errors['customint1'] = get_string('required'); } $validgroups = array_keys($this->get_group_options($context)); $tovalidate = array( 'customint2' => $validgroups ); $typeerrors = $this->validate_param_types($data, $tovalidate); $errors = array_merge($errors, $typeerrors); return $errors; } /** * Restore instance and map settings. * * @param restore_enrolments_structure_step $step * @param stdClass $data * @param stdClass $course * @param int $oldid */ public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { global $DB, $CFG; if (!$step->get_task()->is_samesite()) { // No meta restore from other sites. $step->set_mapping('enrol', $oldid, 0); return; } if (!empty($data->customint2)) { $data->customint2 = $step->get_mappingid('group', $data->customint2); } if ($DB->record_exists('course', array('id' => $data->customint1))) { $instance = $DB->get_record('enrol', array('roleid' => $data->roleid, 'customint1' => $data->customint1, 'courseid' => $course->id, 'enrol' => $this->get_name())); if ($instance) { $instanceid = $instance->id; } else { $instanceid = $this->add_instance($course, (array)$data); } $step->set_mapping('enrol', $oldid, $instanceid); require_once("$CFG->dirroot/enrol/meta/locallib.php"); enrol_meta_sync($data->customint1); } else { $step->set_mapping('enrol', $oldid, 0); } } /** * Restore user enrolment. * * @param restore_enrolments_structure_step $step * @param stdClass $data * @param stdClass $instance * @param int $userid * @param int $oldinstancestatus */ public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { global $DB; if ($this->get_config('unenrolaction') != ENROL_EXT_REMOVED_SUSPENDNOROLES) { // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers. return; } // ENROL_EXT_REMOVED_SUSPENDNOROLES means all previous enrolments are restored // but without roles and suspended. if (!$DB->record_exists('user_enrolments', array('enrolid' => $instance->id, 'userid' => $userid))) { $this->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, ENROL_USER_SUSPENDED); if ($instance->customint2) { groups_add_member($instance->customint2, $userid, 'enrol_meta', $instance->id); } } } /** * Restore user group membership. * @param stdClass $instance * @param int $groupid * @param int $userid */ public function restore_group_member($instance, $groupid, $userid) { // Nothing to do here, the group members are added in $this->restore_group_restored(). return; } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка