See Release Notes
Long Term Support Release
<?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 stdClass The context in which this rating exists> * @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'); } }