ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç rate_ajax.php000064400000005277152265255740007243 0ustar00. /** * This page receives ajax rating submissions * * It is similar to rate.php. Unlike rate.php a return url is NOT required. * * @package core_rating * @category rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require_once('../config.php'); require_once($CFG->dirroot.'/rating/lib.php'); $contextid = required_param('contextid', PARAM_INT); $component = required_param('component', PARAM_COMPONENT); $ratingarea = required_param('ratingarea', PARAM_AREA); $itemid = required_param('itemid', PARAM_INT); $scaleid = required_param('scaleid', PARAM_INT); $userrating = required_param('rating', PARAM_INT); $rateduserid = required_param('rateduserid', PARAM_INT); // The user being rated. Required to update their grade. $aggregationmethod = optional_param('aggregation', RATING_AGGREGATE_NONE, PARAM_INT); // Used to calculate the aggregate to return. $result = new stdClass; // If session has expired and its an ajax request so we cant do a page redirect. if (!isloggedin()) { $result->error = get_string('sessionerroruser', 'error'); echo json_encode($result); die(); } list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm); $contextid = null; // Now we have a context object, throw away the id from the user. $PAGE->set_context($context); $PAGE->set_url('/rating/rate_ajax.php', array('contextid' => $context->id)); if (!confirm_sesskey() || !has_capability('moodle/rating:rate', $context)) { echo $OUTPUT->header(); echo get_string('ratepermissiondenied', 'rating'); echo $OUTPUT->footer(); die(); } $rm = new rating_manager(); $result = $rm->add_rating($cm, $context, $component, $ratingarea, $itemid, $scaleid, $userrating, $rateduserid, $aggregationmethod); // Return translated error. if (!empty($result->error)) { $result->error = get_string($result->error, 'rating'); } echo json_encode($result); lib.php000064400000150107152265255740006044 0ustar00. /** * 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 (note that 'cmidnumber' is required in order to update grades). $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance)); if ($modinstance) { $modinstance->cmidnumber = $cm->idnumber; $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'); } } rate.php000064400000010212152265255740006221 0ustar00. /** * This page receives non-ajax rating submissions * * It is similar to rate_ajax.php. Unlike rate_ajax.php a return url is required. * * @package core_rating * @category rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->dirroot.'/rating/lib.php'); $contextid = required_param('contextid', PARAM_INT); $component = required_param('component', PARAM_COMPONENT); $ratingarea = required_param('ratingarea', PARAM_AREA); $itemid = required_param('itemid', PARAM_INT); $scaleid = required_param('scaleid', PARAM_INT); $userrating = required_param('rating', PARAM_INT); $rateduserid = required_param('rateduserid', PARAM_INT); // Which user is being rated. Required to update their grade. $returnurl = required_param('returnurl', PARAM_LOCALURL); // Required for non-ajax requests. $result = new stdClass; list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm); $contextid = null; // Now we have a context object, throw away the id from the user. $PAGE->set_context($context); $PAGE->set_url('/rating/rate.php', array('contextid' => $context->id)); if (!confirm_sesskey() || !has_capability('moodle/rating:rate', $context)) { throw new \moodle_exception('ratepermissiondenied', 'rating'); } $rm = new rating_manager(); // Check the module rating permissions. // Doing this check here rather than within rating_manager::get_ratings() so we can choose how to handle the error. $pluginpermissionsarray = $rm->get_plugin_permissions_array($context->id, $component, $ratingarea); if (!$pluginpermissionsarray['rate']) { throw new \moodle_exception('ratepermissiondenied', 'rating'); } else { $params = array( 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid, 'rating' => $userrating, 'rateduserid' => $rateduserid ); if (!$rm->check_rating_is_valid($params)) { echo $OUTPUT->header(); echo get_string('ratinginvalid', 'rating'); echo $OUTPUT->footer(); die(); } } if ($userrating != RATING_UNSET_RATING) { $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->component = $component; $ratingoptions->ratingarea = $ratingarea; $ratingoptions->itemid = $itemid; $ratingoptions->scaleid = $scaleid; $ratingoptions->userid = $USER->id; $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; $rm->delete_ratings($options); } if (!empty($cm) && $context->contextlevel == CONTEXT_MODULE) { // Tell the module that its grades have changed (note that 'cmidnumber' is required in order to update grades). $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); $modinstance->cmidnumber = $cm->idnumber; $functionname = $cm->modname.'_update_grades'; require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php"); if (function_exists($functionname)) { $functionname($modinstance, $rateduserid); } } redirect($returnurl); classes/external.php000064400000030203152265255740010547 0ustar00. /** * Rating external API * * @package core_rating * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ defined('MOODLE_INTERNAL') || die; require_once("$CFG->dirroot/rating/lib.php"); use core_external\external_api; use core_external\external_value; use core_external\external_single_structure; use core_external\external_multiple_structure; use core_external\external_function_parameters; use core_external\external_warnings; /** * Rating external functions * * @package core_rating * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ class core_rating_external extends external_api { /** * Returns description of get_item_ratings parameters. * * @return external_function_parameters * @since Moodle 2.9 */ public static function get_item_ratings_parameters() { return new external_function_parameters ( array( 'contextlevel' => new external_value(PARAM_ALPHA, 'context level: course, module, user, etc...'), 'instanceid' => new external_value(PARAM_INT, 'the instance id of item associated with the context level'), 'component' => new external_value(PARAM_COMPONENT, 'component'), 'ratingarea' => new external_value(PARAM_AREA, 'rating area'), 'itemid' => new external_value(PARAM_INT, 'associated id'), 'scaleid' => new external_value(PARAM_INT, 'scale id'), 'sort' => new external_value(PARAM_ALPHA, 'sort order (firstname, rating or timemodified)') ) ); } /** * Retrieve a list of ratings for a given item (forum post etc) * * @param string $contextlevel course, module, user... * @param int $instanceid the instance if for the context element * @param string $component the name of the component * @param string $ratingarea rating area * @param int $itemid the item id * @param int $scaleid the scale id * @param string $sort sql order (firstname, rating or timemodified) * @return array Result and possible warnings * @throws moodle_exception * @since Moodle 2.9 */ public static function get_item_ratings($contextlevel, $instanceid, $component, $ratingarea, $itemid, $scaleid, $sort) { global $USER, $PAGE; $warnings = array(); $arrayparams = array( 'contextlevel' => $contextlevel, 'instanceid' => $instanceid, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid, 'sort' => $sort ); // Validate and normalize parameters. $params = self::validate_parameters(self::get_item_ratings_parameters(), $arrayparams); $context = self::get_context_from_params($params); self::validate_context($context); // Minimal capability required. $callbackparams = array('contextid' => $context->id, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid); if (!has_capability('moodle/rating:view', $context) || !component_callback($component, 'rating_can_see_item_ratings', array($callbackparams), true)) { throw new moodle_exception('noviewrate', 'rating'); } list($context, $course, $cm) = get_context_info_array($context->id); // Can we see all ratings? $canviewallratings = has_capability('moodle/rating:viewall', $context); // Create the Sql sort order string. switch ($params['sort']) { case 'firstname': $sqlsort = "u.firstname ASC"; break; case 'rating': $sqlsort = "r.rating ASC"; break; default: $sqlsort = "r.timemodified ASC"; } $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->component = $params['component']; $ratingoptions->ratingarea = $params['ratingarea']; $ratingoptions->itemid = $params['itemid']; $ratingoptions->sort = $sqlsort; $rm = new rating_manager(); $ratings = $rm->get_all_ratings_for_item($ratingoptions); $scalemenu = make_grades_menu($params['scaleid']); // If the scale was changed after ratings were submitted some ratings may have a value above the current maximum. // We can't just do count($scalemenu) - 1 as custom scales start at index 1, not 0. $maxrating = max(array_keys($scalemenu)); $results = array(); foreach ($ratings as $rating) { if ($canviewallratings || $USER->id == $rating->userid) { if ($rating->rating > $maxrating) { $rating->rating = $maxrating; } $result = array(); $result['id'] = $rating->id; $result['userid'] = $rating->userid; $result['userfullname'] = fullname($rating); $result['rating'] = $scalemenu[$rating->rating]; $result['timemodified'] = $rating->timemodified; // The rating object has all the required fields for generating the picture url. // Undo the aliasing of the user id column from fields::get_sql. $rating->id = $rating->userid; $userpicture = new user_picture($rating); $userpicture->size = 1; // Size f1. $result['userpictureurl'] = $userpicture->get_url($PAGE)->out(false); $results[] = $result; } } return array( 'ratings' => $results, 'warnings' => $warnings ); } /** * Returns description of get_item_ratings result values. * * @return external_single_structure * @since Moodle 2.9 */ public static function get_item_ratings_returns() { return new external_single_structure( array( 'ratings' => new external_multiple_structure( new external_single_structure( array( 'id' => new external_value(PARAM_INT, 'rating id'), 'userid' => new external_value(PARAM_INT, 'user id'), 'userpictureurl' => new external_value(PARAM_URL, 'URL user picture'), 'userfullname' => new external_value(PARAM_NOTAGS, 'user fullname'), 'rating' => new external_value(PARAM_NOTAGS, 'rating on scale'), 'timemodified' => new external_value(PARAM_INT, 'time modified (timestamp)') ), 'Rating' ), 'list of ratings' ), 'warnings' => new external_warnings(), ) ); } /** * Returns description of add_rating parameters. * * @return external_function_parameters * @since Moodle 3.2 */ public static function add_rating_parameters() { return new external_function_parameters ( array( 'contextlevel' => new external_value(PARAM_ALPHA, 'context level: course, module, user, etc...'), 'instanceid' => new external_value(PARAM_INT, 'the instance id of item associated with the context level'), 'component' => new external_value(PARAM_COMPONENT, 'component'), 'ratingarea' => new external_value(PARAM_AREA, 'rating area'), 'itemid' => new external_value(PARAM_INT, 'associated id'), 'scaleid' => new external_value(PARAM_INT, 'scale id'), 'rating' => new external_value(PARAM_INT, 'user rating'), 'rateduserid' => new external_value(PARAM_INT, 'rated user id'), 'aggregation' => new external_value(PARAM_INT, 'agreggation method', VALUE_DEFAULT, RATING_AGGREGATE_NONE) ) ); } /** * Adds a rating to an item * * @param string $contextlevel course, module, user... * @param int $instanceid the instance if for the context element * @param string $component the name of the component * @param string $ratingarea rating area * @param int $itemid the item id * @param int $scaleid the scale id * @param int $rating the user rating * @param int $rateduserid the rated user id * @param int $aggregation the aggregation method * @return array result and possible warnings * @throws moodle_exception * @since Moodle 3.2 */ public static function add_rating($contextlevel, $instanceid, $component, $ratingarea, $itemid, $scaleid, $rating, $rateduserid, $aggregation = RATING_AGGREGATE_NONE) { $warnings = array(); $params = array( 'contextlevel' => $contextlevel, 'instanceid' => $instanceid, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid, 'rating' => $rating, 'rateduserid' => $rateduserid, 'aggregation' => $aggregation, ); // Validate and normalize parameters. $params = self::validate_parameters(self::add_rating_parameters(), $params); $context = self::get_context_from_params($params); self::validate_context($context); $cm = get_coursemodule_from_id(false, $context->instanceid, 0, false, MUST_EXIST); require_capability('moodle/rating:rate', $context); $rm = new rating_manager(); $result = $rm->add_rating($cm, $context, $params['component'], $params['ratingarea'], $params['itemid'], $params['scaleid'], $params['rating'], $params['rateduserid'], $params['aggregation']); if (!empty($result->error)) { throw new moodle_exception($result->error, 'rating'); } $returndata = array( 'success' => $result->success, 'warnings' => $warnings ); if (isset($result->aggregate)) { $returndata['aggregate'] = $result->aggregate; $returndata['count'] = $result->count; $returndata['itemid'] = $result->itemid; } return $returndata; } /** * Returns description of add_rating result values. * * @return external_single_structure * @since Moodle 3.2 */ public static function add_rating_returns() { return new external_single_structure( array( 'success' => new external_value(PARAM_BOOL, 'Whether the rate was successfully created'), 'aggregate' => new external_value(PARAM_TEXT, 'New aggregate', VALUE_OPTIONAL), 'count' => new external_value(PARAM_INT, 'Ratings count', VALUE_OPTIONAL), 'itemid' => new external_value(PARAM_INT, 'Rating item id', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ) ); } } classes/external/util.php000064400000022214152265255740011527 0ustar00. /** * Rating external functions utility class. * * @package core_rating * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_rating\external; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/rating/lib.php'); use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use rating_manager; use stdClass; /** * Rating external functions utility class. * * @package core_rating * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.4 */ class util { /** * Returns the ratings definition for external functions. */ public static function external_ratings_structure() { return new external_single_structure ( [ 'contextid' => new external_value(PARAM_INT, 'Context id.'), 'component' => new external_value(PARAM_COMPONENT, 'Context name.'), 'ratingarea' => new external_value(PARAM_AREA, 'Rating area name.'), 'canviewall' => new external_value(PARAM_BOOL, 'Whether the user can view all the individual ratings.', VALUE_OPTIONAL), 'canviewany' => new external_value(PARAM_BOOL, 'Whether the user can view aggregate of ratings of others.', VALUE_OPTIONAL), 'scales' => new external_multiple_structure( new external_single_structure ( [ 'id' => new external_value(PARAM_INT, 'Scale id.'), 'courseid' => new external_value(PARAM_INT, 'Course id.', VALUE_OPTIONAL), 'name' => new external_value(PARAM_TEXT, 'Scale name (when a real scale is used).', VALUE_OPTIONAL), 'max' => new external_value(PARAM_INT, 'Max value for the scale.'), 'isnumeric' => new external_value(PARAM_BOOL, 'Whether is a numeric scale.'), 'items' => new external_multiple_structure( new external_single_structure ( [ 'value' => new external_value(PARAM_INT, 'Scale value/option id.'), 'name' => new external_value(PARAM_NOTAGS, 'Scale name.'), ] ), 'Scale items. Only returned for not numerical scales.', VALUE_OPTIONAL ) ], 'Scale information' ), 'Different scales used information', VALUE_OPTIONAL ), 'ratings' => new external_multiple_structure( new external_single_structure ( [ 'itemid' => new external_value(PARAM_INT, 'Item id.'), 'scaleid' => new external_value(PARAM_INT, 'Scale id.', VALUE_OPTIONAL), 'userid' => new external_value(PARAM_INT, 'User who rated id.', VALUE_OPTIONAL), 'aggregate' => new external_value(PARAM_FLOAT, 'Aggregated ratings grade.', VALUE_OPTIONAL), 'aggregatestr' => new external_value(PARAM_NOTAGS, 'Aggregated ratings as string.', VALUE_OPTIONAL), 'aggregatelabel' => new external_value(PARAM_NOTAGS, 'The aggregation label.', VALUE_OPTIONAL), 'count' => new external_value(PARAM_INT, 'Ratings count (used when aggregating).', VALUE_OPTIONAL), 'rating' => new external_value(PARAM_INT, 'The rating the user gave.', VALUE_OPTIONAL), 'canrate' => new external_value(PARAM_BOOL, 'Whether the user can rate the item.', VALUE_OPTIONAL), 'canviewaggregate' => new external_value(PARAM_BOOL, 'Whether the user can view the aggregated grade.', VALUE_OPTIONAL), ] ), 'The ratings', VALUE_OPTIONAL ), ], 'Rating information', VALUE_OPTIONAL ); } /** * Returns rating information inside a data structure like the one defined by external_ratings_structure. * * @param stdClass $mod course module object * @param stdClass $context context object * @param str $component component name * @param str $ratingarea rating area * @param array $items items to add ratings * @return array ratings ready to be returned by external functions. */ public static function get_rating_info($mod, $context, $component, $ratingarea, $items) { global $USER; $ratinginfo = [ 'contextid' => $context->id, 'component' => $component, 'ratingarea' => $ratingarea, 'canviewall' => null, 'canviewany' => null, 'scales' => [], 'ratings' => [], ]; if ($mod->assessed != RATING_AGGREGATE_NONE) { $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->component = $component; $ratingoptions->ratingarea = $ratingarea; $ratingoptions->items = $items; $ratingoptions->aggregate = $mod->assessed; $ratingoptions->scaleid = $mod->scale; $ratingoptions->userid = $USER->id; $ratingoptions->assesstimestart = $mod->assesstimestart; $ratingoptions->assesstimefinish = $mod->assesstimefinish; $rm = new rating_manager(); $allitems = $rm->get_ratings($ratingoptions); foreach ($allitems as $item) { if (empty($item->rating)) { continue; } $rating = [ 'itemid' => $item->rating->itemid, 'scaleid' => $item->rating->scaleid, 'userid' => $item->rating->userid, 'rating' => $item->rating->rating, 'canrate' => $item->rating->user_can_rate(), 'canviewaggregate' => $item->rating->user_can_view_aggregate(), ]; // Fill the capabilities fields the first time (the rest are the same values because they are not item dependent). if ($ratinginfo['canviewall'] === null) { $ratinginfo['canviewall'] = $item->rating->settings->permissions->viewall && $item->rating->settings->pluginpermissions->viewall; $ratinginfo['canviewany'] = $item->rating->settings->permissions->viewany && $item->rating->settings->pluginpermissions->viewany; } // Return only the information the user can see. if ($rating['canviewaggregate']) { $rating['aggregate'] = $item->rating->aggregate; $rating['aggregatestr'] = $item->rating->get_aggregate_string(); $rating['aggregatelabel'] = $rm->get_aggregate_label($item->rating->settings->aggregationmethod); $rating['count'] = $item->rating->count; } // If the user can rate, return the scale information only one time. if ($rating['canrate'] && !empty($item->rating->settings->scale->id) && !isset($ratinginfo['scales'][$item->rating->settings->scale->id])) { $scale = $item->rating->settings->scale; // Return only non numeric scales (to avoid return lots of data just including items from 0 to $scale->max). if (!$scale->isnumeric) { $scaleitems = []; foreach ($scale->scaleitems as $value => $name) { $scaleitems[] = [ 'name' => $name, 'value' => $value, ]; } $scale->items = $scaleitems; } $ratinginfo['scales'][$item->rating->settings->scale->id] = (array) $scale; } $ratinginfo['ratings'][] = $rating; } } return $ratinginfo; } } classes/privacy/provider.php000064400000025103152265255740012237 0ustar00. /** * Privacy Subsystem implementation for core_ratings. * * @package core_rating * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_rating\privacy; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\userlist; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/rating/lib.php'); /** * Privacy Subsystem implementation for core_ratings. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // The ratings subsystem contains data. \core_privacy\local\metadata\provider, // The ratings subsystem is only ever used to store data for other components. // It does not store any data of its own and does not need to implement the \core_privacy\local\request\subsystem\provider // as a result. // The ratings subsystem provides a data service to other components. \core_privacy\local\request\subsystem\plugin_provider, \core_privacy\local\request\shared_userlist_provider { /** * Returns metadata about the ratings subsystem. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through the subsystem. */ public static function get_metadata(collection $collection): collection { // The table 'rating' cotains data that a user has entered. // It stores the user-entered rating alongside a mapping to describe what was mapped. $collection->add_database_table('rating', [ 'rating' => 'privacy:metadata:rating:rating', 'userid' => 'privacy:metadata:rating:userid', 'timecreated' => 'privacy:metadata:rating:timecreated', 'timemodified' => 'privacy:metadata:rating:timemodified', ], 'privacy:metadata:rating'); return $collection; } /** * Export all ratings which match the specified component, areaid, and itemid. * * If requesting ratings for a users own content, and you wish to include all ratings of that content, specify * $onlyuser as false. * * When requesting ratings for another users content, you should only export the ratings that the specified user * made themselves. * * @param int $userid The user whose information is to be exported * @param \context $context The context being stored. * @param array $subcontext The subcontext within the context to export this information * @param string $component The component to fetch data from * @param string $ratingarea The ratingarea that the data was stored in within the component * @param int $itemid The itemid within that ratingarea * @param bool $onlyuser Whether to only export ratings that the current user has made, or all ratings */ public static function export_area_ratings( int $userid, \context $context, array $subcontext, string $component, string $ratingarea, int $itemid, bool $onlyuser = true ) { global $DB; $rm = new \rating_manager(); $ratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, ]); if ($onlyuser) { $ratings = array_filter($ratings, function($rating) use ($userid){ return ($rating->userid == $userid); }); } if (empty($ratings)) { return; } $toexport = array_map(function($rating) { return (object) [ 'rating' => $rating->rating, 'author' => $rating->userid, ]; }, $ratings); $writer = \core_privacy\local\request\writer::with_context($context) ->export_related_data($subcontext, 'rating', $toexport); } /** * Get the SQL required to find all submission items where this user has had any involvements. * * If possible an inner join should be used. * * @param string $alias The name of the table alias to use. * @param string $component The na eof the component to fetch ratings for. * @param string $ratingarea The rating area to fetch results for. * @param string $itemidjoin The right-hand-side of the JOIN ON clause. * @param int $userid The ID of the user being stored. * @param bool $innerjoin Whether to use an inner join (preferred) * @return \stdClass */ public static function get_sql_join($alias, $component, $ratingarea, $itemidjoin, $userid, $innerjoin = false) { static $count = 0; $count++; $userwhere = ''; if ($innerjoin) { // Join the rating table with the specified alias and the relevant join params. $join = "JOIN {rating} {$alias} ON "; $join .= "{$alias}.itemid = {$itemidjoin}"; $userwhere .= "{$alias}.userid = :ratinguserid{$count} AND "; $userwhere .= "{$alias}.component = :ratingcomponent{$count} AND "; $userwhere .= "{$alias}.ratingarea = :ratingarea{$count}"; } else { // Join the rating table with the specified alias and the relevant join params. $join = "LEFT JOIN {rating} {$alias} ON "; $join .= "{$alias}.userid = :ratinguserid{$count} AND "; $join .= "{$alias}.component = :ratingcomponent{$count} AND "; $join .= "{$alias}.ratingarea = :ratingarea{$count} AND "; $join .= "{$alias}.itemid = {$itemidjoin}"; // Match against the specified user. $userwhere = "{$alias}.id IS NOT NULL"; } $params = [ 'ratingcomponent' . $count => $component, 'ratingarea' . $count => $ratingarea, 'ratinguserid' . $count => $userid, ]; $return = (object) [ 'join' => $join, 'params' => $params, 'userwhere' => $userwhere, ]; return $return; } /** * Deletes all ratings for a specified context, component, ratingarea and itemid. * * Only delete ratings when the item itself was deleted. * * We never delete ratings for one user but not others - this may affect grades, therefore ratings * made by particular user are not considered personal information. * * @param \context $context Details about which context to delete ratings for. * @param string $component Component to delete. * @param string $ratingarea Rating area to delete. * @param int $itemid The item ID for use with deletion. */ public static function delete_ratings(\context $context, ?string $component = null, ?string $ratingarea = null, ?int $itemid = null) { global $DB; $options = ['contextid' => $context->id]; if ($component) { $options['component'] = $component; } if ($ratingarea) { $options['ratingarea'] = $ratingarea; } if ($itemid) { $options['itemid'] = $itemid; } $DB->delete_records('rating', $options); } /** * Deletes all tag instances for given context, component, itemtype using subquery for itemids * * In most situations you will want to specify $userid as null. Per-user tag instances * are possible in Tags API, however there are no components or standard plugins that actually use them. * * @param \context $context Details about which context to delete ratings for. * @param string $component Component to delete. * @param string $ratingarea Rating area to delete. * @param string $itemidstest an SQL fragment that the itemid must match. Used * in the query like WHERE itemid $itemidstest. Must use named parameters, * and may not use named parameters called contextid, component or ratingarea. * @param array $params any query params used by $itemidstest. */ public static function delete_ratings_select(\context $context, string $component, string $ratingarea, $itemidstest, $params = []) { global $DB; $params += ['contextid' => $context->id, 'component' => $component, 'ratingarea' => $ratingarea]; $DB->delete_records_select('rating', 'contextid = :contextid AND component = :component AND ratingarea = :ratingarea AND itemid ' . $itemidstest, $params); } /** * Add the list of users who have rated in the specified constraints. * * @param userlist $userlist The userlist to add the users to. * @param string $alias An alias prefix to use for rating selects to avoid interference with your own sql. * @param string $component The component to check. * @param string $area The rating area to check. * @param string $insql The SQL to use in a sub-select for the itemid query. * @param array $params The params required for the insql. */ public static function get_users_in_context_from_sql( userlist $userlist, string $alias, string $component, string $area, string $insql, $params) { // Discussion authors. $sql = "SELECT {$alias}.userid FROM {rating} {$alias} WHERE {$alias}.component = :{$alias}component AND {$alias}.ratingarea = :{$alias}ratingarea AND {$alias}.itemid IN ({$insql})"; $params["{$alias}component"] = $component; $params["{$alias}ratingarea"] = $area; $userlist->add_from_sql('userid', $sql, $params); } } classes/phpunit/privacy_helper.php000064400000012106152265255740013432 0ustar00. /** * Helpers for the core_rating subsystem implementation of privacy. * * @package core_rating * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_rating\phpunit; defined('MOODLE_INTERNAL') || die(); use \core_privacy\tests\request\content_writer; global $CFG; /** * Helpers for the core_rating subsystem implementation of privacy. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait privacy_helper { /** * Fetch all ratings on a subcontext. * * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @return \stdClass|array */ protected function get_ratings_on_subcontext(\context $context, array $subcontext) { $writer = \core_privacy\local\request\writer::with_context($context); return $writer->get_related_data($subcontext, 'rating'); } /** * Check that all included ratings belong to the specified user. * * @param int $userid The ID of the user being stored. * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @param string $component The component being stored. * @param string $ratingarea The rating area to store results for. * @param int $itemid The itemid to store. */ protected function assert_all_own_ratings_on_context( int $userid, \context $context, array $subcontext, $component, $ratingarea, $itemid ) { $writer = \core_privacy\local\request\writer::with_context($context); $rm = new \rating_manager(); $dbratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, ]); $exportedratings = $this->get_ratings_on_subcontext($context, $subcontext); foreach ($exportedratings as $ratingid => $rating) { $this->assertTrue(isset($dbratings[$ratingid])); $this->assertEquals($userid, $rating->author); $this->assert_rating_matches($dbratings[$ratingid], $rating); } foreach ($dbratings as $rating) { if ($rating->userid == $userid) { $this->assertEquals($rating->id, $ratingid); } } } /** * Check that all included ratings are valid. They may belong to any user. * * @param \context $context The context being stored. * @param array $subcontext The subcontext path to check. * @param string $component The component being stored. * @param string $ratingarea The rating area to store results for. * @param int $itemid The itemid to store. */ protected function assert_all_ratings_on_context(\context $context, array $subcontext, $component, $ratingarea, $itemid) { $writer = \core_privacy\local\request\writer::with_context($context); $rm = new \rating_manager(); $dbratings = $rm->get_all_ratings_for_item((object) [ 'context' => $context, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, ]); $exportedratings = $this->get_ratings_on_subcontext($context, $subcontext); foreach ($exportedratings as $ratingid => $rating) { $this->assertTrue(isset($dbratings[$ratingid])); $this->assert_rating_matches($dbratings[$ratingid], $rating); } foreach ($dbratings as $rating) { $this->assertTrue(isset($exportedratings[$rating->id])); } } /** * Assert that the rating matches. * * @param \stdClass $expected The expected rating structure * @param \stdClass $stored The actual rating structure */ protected function assert_rating_matches($expected, $stored) { $this->assertEquals($expected->rating, $stored->rating); $this->assertEquals($expected->userid, $stored->author); } } module.js000064400000005442152265255740006411 0ustar00M.core_rating = { Y : null, api: M.cfg.wwwroot + '/rating/rate_ajax.php', init : function(Y){ this.Y = Y; Y.all('select.postratingmenu').each(this.attach_rating_events, this); //hide the submit buttons this.Y.all('input.postratingmenusubmit').setStyle('display', 'none'); }, attach_rating_events : function(selectnode) { selectnode.on('change', this.submit_rating, this, selectnode); }, submit_rating : function(e, selectnode){ var theinputs = selectnode.ancestor('form').all('.ratinginput'); var thedata = []; var inputssize = theinputs.size(); for (var i = 0; i < inputssize; i++) { if(theinputs.item(i).get("name") != "returnurl") { // Dont include return url for ajax requests. thedata[theinputs.item(i).get("name")] = theinputs.item(i).get("value"); } } var scope = this; var cfg = { method: 'POST', on: { complete : function(tid, outcome, args) { try { if (!outcome) { alert('IO FATAL'); return false; } var data = scope.Y.JSON.parse(outcome.responseText); if (data.success){ //if the user has access to the aggregate then update it if (data.itemid) { //do not test data.aggregate or data.count otherwise it doesn't refresh value=0 or no value var itemid = data.itemid; var node = scope.Y.one('#ratingaggregate' + itemid); node.set('innerHTML',data.aggregate); // Empty the count value if no ratings. var node = scope.Y.one('#ratingcount' + itemid); if (data.count > 0) { node.set('innerHTML', "(" + data.count + ")"); } else { node.set('innerHTML', ""); } } return true; } else if (data.error) { alert(data.error); } } catch(e) { alert(e.message + " " + outcome.responseText); } return false; } }, arguments: { scope: scope }, headers: { }, data: build_querystring(thedata) }; this.Y.io(this.api, cfg); } }; index.php000064400000012707152265255740006410 0ustar00. /** * A page to display a list of ratings for a given item (forum post etc) * * @package core_rating * @category rating * @copyright 2010 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../config.php"); require_once("lib.php"); $contextid = required_param('contextid', PARAM_INT); $component = required_param('component', PARAM_COMPONENT); $ratingarea = required_param('ratingarea', PARAM_AREA); $itemid = required_param('itemid', PARAM_INT); $scaleid = required_param('scaleid', PARAM_INT); $sort = optional_param('sort', '', PARAM_ALPHA); $popup = optional_param('popup', 0, PARAM_INT); // Any non-zero value if in a popup window. list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm); $url = new moodle_url('/rating/index.php', array('contextid' => $contextid, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid)); if (!empty($sort)) { $url->param('sort', $sort); } if (!empty($popup)) { $url->param('popup', $popup); } $PAGE->set_url($url); $PAGE->set_context($context); if ($popup) { $PAGE->set_pagelayout('popup'); } $params = array('contextid' => $contextid, 'component' => $component, 'ratingarea' => $ratingarea, 'itemid' => $itemid, 'scaleid' => $scaleid); if (!has_capability('moodle/rating:view', $context) || !component_callback($component, 'rating_can_see_item_ratings', array($params), true)) { throw new \moodle_exception('noviewrate', 'rating'); } $canviewallratings = has_capability('moodle/rating:viewall', $context); switch ($sort) { case 'firstname': $sqlsort = "u.firstname ASC"; break; case 'rating': $sqlsort = "r.rating ASC"; break; default: $sqlsort = "r.timemodified ASC"; } $scalemenu = make_grades_menu($scaleid); $strrating = get_string('rating', 'rating'); $strname = get_string('name'); $strtime = get_string('time'); $PAGE->set_title(get_string('allratingsforitem', 'rating')); echo $OUTPUT->header(); $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->component = $component; $ratingoptions->ratingarea = $ratingarea; $ratingoptions->itemid = $itemid; $ratingoptions->sort = $sqlsort; $rm = new rating_manager(); $ratings = $rm->get_all_ratings_for_item($ratingoptions); if (!$ratings) { $msg = get_string('noratings', 'rating'); echo html_writer::tag('div', $msg, array('class' => 'mdl-align')); } else { // To get the sort URL, copy the current URL and remove any previous sort. $sorturl = new moodle_url($url); $sorturl->remove_params('sort'); $table = new html_table; $table->cellpadding = 3; $table->cellspacing = 3; $table->attributes['class'] = 'generalbox ratingtable table-reboot'; $table->head = array( '', html_writer::link(new moodle_url($sorturl, array('sort' => 'firstname')), $strname), html_writer::link(new moodle_url($sorturl, array('sort' => 'rating')), $strrating), html_writer::link(new moodle_url($sorturl, array('sort' => 'time')), $strtime) ); $table->colclasses = array('', 'firstname', 'rating', 'time'); $table->data = array(); // If the scale was changed after ratings were submitted some ratings may have a value above the current maximum. // We can't just do count($scalemenu) - 1 as custom scales start at index 1, not 0. $maxrating = max(array_keys($scalemenu)); foreach ($ratings as $rating) { if (!$canviewallratings and $USER->id != $rating->userid) { continue; } // Undo the aliasing of the user id column from fields::get_sql(). // We could clone the rating object or preserve the rating id if we needed it again // but we don't. $rating->id = $rating->userid; $row = new html_table_row(); $row->attributes['class'] = 'ratingitemheader'; if ($course && $course->id) { $row->cells[] = $OUTPUT->user_picture($rating, array('courseid' => $course->id)); } else { $row->cells[] = $OUTPUT->user_picture($rating); } $row->cells[] = fullname($rating); if ($rating->rating > $maxrating) { $rating->rating = $maxrating; } $row->cells[] = $scalemenu[$rating->rating]; $row->cells[] = userdate($rating->timemodified); $table->data[] = $row; } echo html_writer::table($table); } if ($popup) { echo $OUTPUT->close_window_button(); } echo $OUTPUT->footer(); tests/rating_test.php000064400000040621152265255740010762 0ustar00. namespace core_rating; use rating_manager; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff. global $CFG; require_once($CFG->dirroot . '/rating/lib.php'); /** * Unit test case for all the rating/lib.php requiring DB mockup & manipulation * * @package core_rating * @category test * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \rating */ final class rating_test extends \advanced_testcase { protected $syscontext; protected $neededcaps = array('view', 'viewall', 'viewany', 'rate'); protected $originaldefaultfrontpageroleid; public function setUp(): void { global $CFG; parent::setUp(); $this->resetAfterTest(true); $CFG->defaultfrontpageroleid = null; } /** * Test the current get_ratings method main sql */ public function test_get_ratings_sql(): void { global $DB; // We load 3 items. Each is rated twice. For simplicity itemid == user id of the item owner. $ctxid = \context_system::instance()->id; $ratings = array( // User 1's items. Average == 2. array('contextid' => $ctxid, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => 1, 'scaleid' => 10, 'rating' => 1, 'userid' => 2, 'timecreated' => 1, 'timemodified' => 1), array('contextid' => $ctxid, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => 1, 'scaleid' => 10, 'rating' => 3, 'userid' => 3, 'timecreated' => 1, 'timemodified' => 1), // User 2's items. Average == 3. array('contextid' => $ctxid, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => 2, 'scaleid' => 10, 'rating' => 1, 'userid' => 1, 'timecreated' => 1, 'timemodified' => 1), array('contextid' => $ctxid, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => 2, 'scaleid' => 10, 'rating' => 4, 'userid' => 3, 'timecreated' => 1, 'timemodified' => 1), // User 3's items. Average == 4. array('contextid' => $ctxid, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => 3, 'scaleid' => 10, 'rating' => 3, 'userid' => 1, 'timecreated' => 1, 'timemodified' => 1), array('contextid' => $ctxid, 'component' => 'mod_forum', 'ratingarea' => 'post', 'itemid' => 3, 'scaleid' => 10, 'rating' => 5, 'userid' => 2, 'timecreated' => 1, 'timemodified' => 1) ); foreach ($ratings as $rating) { $DB->insert_record('rating', $rating); } // A post (item) by user 1 (rated above by user 2 and 3 with average = 2). $user1posts = array( (object)array('id' => 1, 'userid' => 1, 'message' => 'hello')); // A post (item) by user 2 (rated above by user 1 and 3 with average = 3). $user2posts = array( (object)array('id' => 2, 'userid' => 2, 'message' => 'world')); // A post (item) by user 3 (rated above by user 1 and 2 with average = 4). $user3posts = array( (object)array('id' => 3, 'userid' => 3, 'message' => 'moodle')); // Prepare the default options. $defaultoptions = array ( 'context' => \context_system::instance(), 'component' => 'mod_forum', 'ratingarea' => 'post', 'scaleid' => 10, 'aggregate' => RATING_AGGREGATE_AVERAGE); $rm = new mockup_rating_manager(); // STEP 1: Retreive ratings using the current user. // Get results for user 1's item (expected average 1 + 3 / 2 = 2). $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($user1posts)); $this->assertEquals($result[0]->id, $user1posts[0]->id); $this->assertEquals($result[0]->userid, $user1posts[0]->userid); $this->assertEquals($result[0]->message, $user1posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 2); // Note that $result[0]->rating->rating is somewhat random. // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests. // Get results for items of user 2 (expected average 1 + 4 / 2 = 2.5). $toptions = (object)array_merge($defaultoptions, array('items' => $user2posts)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($user2posts)); $this->assertEquals($result[0]->id, $user2posts[0]->id); $this->assertEquals($result[0]->userid, $user2posts[0]->userid); $this->assertEquals($result[0]->message, $user2posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 2.5); // Note that $result[0]->rating->rating is somewhat random. // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests. // Get results for items of user 3 (expected average 3 + 5 / 2 = 4). $toptions = (object)array_merge($defaultoptions, array('items' => $user3posts)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($user3posts)); $this->assertEquals($result[0]->id, $user3posts[0]->id); $this->assertEquals($result[0]->userid, $user3posts[0]->userid); $this->assertEquals($result[0]->message, $user3posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 4); // Note that $result[0]->rating->rating is somewhat random. // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests. // Get results for items of user 1 & 2 together (expected averages are 2 and 2.5, as tested above). $posts = array_merge($user1posts, $user2posts); $toptions = (object)array_merge($defaultoptions, array('items' => $posts)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($posts)); $this->assertEquals($result[0]->id, $posts[0]->id); $this->assertEquals($result[0]->userid, $posts[0]->userid); $this->assertEquals($result[0]->message, $posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 2); // Note that $result[0]->rating->rating is somewhat random. // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests. $this->assertEquals($result[1]->id, $posts[1]->id); $this->assertEquals($result[1]->userid, $posts[1]->userid); $this->assertEquals($result[1]->message, $posts[1]->message); $this->assertEquals($result[1]->rating->count, 2); $this->assertEquals($result[1]->rating->aggregate, 2.5); // Note that $result[0]->rating->rating is somewhat random. // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests. // STEP 2: Retrieve ratings by a specified user. // We still expect complete aggregations and counts. // Get results for items of user 1 rated by user 2 (avg 2, rating 1). $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts, 'userid' => 2)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($user1posts)); $this->assertEquals($result[0]->id, $user1posts[0]->id); $this->assertEquals($result[0]->userid, $user1posts[0]->userid); $this->assertEquals($result[0]->message, $user1posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 2); $this->assertEquals($result[0]->rating->rating, 1); // User 2 rated user 1 "1". $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid. // Get results for items of user 1 rated by user 3. $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts, 'userid' => 3)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($user1posts)); $this->assertEquals($result[0]->id, $user1posts[0]->id); $this->assertEquals($result[0]->userid, $user1posts[0]->userid); $this->assertEquals($result[0]->message, $user1posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 2); $this->assertEquals($result[0]->rating->rating, 3); // User 3 rated user 1 "3". $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid. // Get results for items of user 1 & 2 together rated by user 3. $posts = array_merge($user1posts, $user2posts); $toptions = (object)array_merge($defaultoptions, array('items' => $posts, 'userid' => 3)); $result = $rm->get_ratings($toptions); $this->assertEquals(count($result), count($posts)); $this->assertEquals($result[0]->id, $posts[0]->id); $this->assertEquals($result[0]->userid, $posts[0]->userid); $this->assertEquals($result[0]->message, $posts[0]->message); $this->assertEquals($result[0]->rating->count, 2); $this->assertEquals($result[0]->rating->aggregate, 2); $this->assertEquals($result[0]->rating->rating, 3); // User 3 rated user 1 "3". $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid. $this->assertEquals($result[1]->id, $posts[1]->id); $this->assertEquals($result[1]->userid, $posts[1]->userid); $this->assertEquals($result[1]->message, $posts[1]->message); $this->assertEquals($result[1]->rating->count, 2); $this->assertEquals($result[1]->rating->aggregate, 2.5); $this->assertEquals($result[0]->rating->rating, 3); // User 3 rated user 2 "5". $this->assertEquals($result[1]->rating->userid, $toptions->userid); // Must be the passed userid. // STEP 3: Some special cases. // Get results for user 1's items (expected average 1 + 3 / 2 = 2). // Supplying a non-existent user id so no rating from that user should be found. $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts)); $toptions->userid = 123456; // Non-existent user. $result = $rm->get_ratings($toptions); $this->assertNull($result[0]->rating->userid); $this->assertNull($result[0]->rating->rating); $this->assertEquals($result[0]->rating->aggregate, 2); // Should still get the aggregate. // Get results for items of user 2 (expected average 1 + 4 / 2 = 2.5). // Supplying the user id of the user who owns the items so no rating should be found. $toptions = (object)array_merge($defaultoptions, array('items' => $user2posts)); $toptions->userid = 2; // User 2 viewing the ratings of their own item. $result = $rm->get_ratings($toptions); // These should be null as the user is viewing their own item and thus cannot rate. $this->assertNull($result[0]->rating->userid); $this->assertNull($result[0]->rating->rating); $this->assertEquals($result[0]->rating->aggregate, 2.5); // Should still get the aggregate. } /** * Data provider for get_aggregate_string tests. * * @return array */ public static function get_aggregate_string_provider(): array { return [ 'Non-numeric aggregate produces empty string' => [ RATING_AGGREGATE_NONE, 'string', null, ['Foo', 'Bar'], '', ], 'Aggregate count produces empty string' => [ RATING_AGGREGATE_COUNT, 0, null, ['Foo', 'Bar'], '', ], 'Numeric SUM with non-numeric scale produces returns original value' => [ RATING_AGGREGATE_SUM, 10, false, ['Foo', 'Bar'], '10', ], 'Numeric SUM with non-numeric scale produces returns rounded value' => [ RATING_AGGREGATE_SUM, 10.45, false, ['Foo', 'Bar'], '10.5', ], 'Numeric SUM with numeric scale produces returns rounded value' => [ RATING_AGGREGATE_SUM, 10.45, true, ['Foo', 'Bar'], '10.5', ], 'Numeric AVERAGE with numeric scale produces returns rounded value' => [ RATING_AGGREGATE_AVERAGE, 10.45, true, ['Foo', 'Bar'], '10.5', ], 'Numeric AVERAGE with non-numeric scale produces returns indexed value (0)' => [ RATING_AGGREGATE_AVERAGE, 0, false, ['Foo', 'Bar'], 'Foo', ], 'Numeric AVERAGE with non-numeric scale produces returns indexed value (1)' => [ RATING_AGGREGATE_AVERAGE, 1, false, ['Foo', 'Bar'], 'Bar', ], ]; } /** * Test the value returned by get_aggregate_string(). * * @dataProvider get_aggregate_string_provider */ public function test_get_aggregate_string($method, $aggregate, $isnumeric, $scaleitems, $expectation): void { $options = new \stdClass(); $options->aggregate = $aggregate; $options->context = null; $options->component = null; $options->ratingarea = null; $options->itemid = null; $options->scaleid = null; $options->userid = null; $options->settings = new \stdClass(); $options->settings->aggregationmethod = $method; $options->settings->scale = new \stdClass(); $options->settings->scale->isnumeric = $isnumeric; $options->settings->scale->scaleitems = $scaleitems; $rating = new \rating($options); $this->assertEquals($expectation, $rating->get_aggregate_string()); } } /** * rating_manager subclass for unit testing without requiring capabilities to be loaded */ class mockup_rating_manager extends rating_manager { /** * Overwrite get_plugin_permissions_array() so it always return granted perms for unit testing */ public function get_plugin_permissions_array($contextid, $component, $ratingarea) { return array( 'rate' => true, 'view' => true, 'viewany' => true, 'viewall' => true); } } tests/privacy/provider_test.php000064400000051666152265255740013020 0ustar00. /** * Unit tests for the core_rating implementation of the Privacy API. * * @package core_rating * @category test * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_rating\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/rating/lib.php'); use core_rating\privacy\provider; use core_privacy\local\request\writer; /** * Unit tests for the core_rating implementation of the Privacy API. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Rate something as a user. * * @param int $userid * @param string $component * @param string $ratingarea * @param int $itemid * @param \context $context * @param string $score */ protected function rate_as_user($userid, $component, $ratingarea, $itemid, $context, $score) { // Rate the courses. $rm = new \rating_manager(); $ratingoptions = (object) [ 'component' => $component, 'ratingarea' => $ratingarea, 'scaleid' => 100, ]; // Rate all courses as u1, and the course category too.. $ratingoptions->itemid = $itemid; $ratingoptions->userid = $userid; $ratingoptions->context = $context; $rating = new \rating($ratingoptions); $rating->update_rating($score); } /** * Ensure that the get_sql_join function returns valid SQL which returns the correct list of rated itemids. */ public function test_get_sql_join(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); // Rate the courses. $rm = new \rating_manager(); $ratingoptions = (object) [ 'component' => 'core_course', 'ratingarea' => 'course', 'scaleid' => 100, ]; // Rate all courses as u1, and something else in the same context. $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25); $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50); $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75); $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99); // Rate course2 as u2, and something else in a different context/component.. $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90); $this->rate_as_user($u2->id, 'user', 'user', $u3->id, \context_user::instance($u3->id), 10); // Return any course which the u1 has rated. // u1 rated all three courses. $ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u1->id); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(3, $courses); $this->assertTrue(isset($courses[$course1->id])); $this->assertTrue(isset($courses[$course2->id])); $this->assertTrue(isset($courses[$course3->id])); // User u1 rated files in course 3 only. $ratingquery = provider::get_sql_join('r', 'core_course', 'files', 'c.id', $u1->id); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(1, $courses); $this->assertFalse(isset($courses[$course1->id])); $this->assertFalse(isset($courses[$course2->id])); $this->assertTrue(isset($courses[$course3->id])); // Return any course which the u2 has rated. // User u2 rated only course 2. $ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u2->id); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(1, $courses); $this->assertFalse(isset($courses[$course1->id])); $this->assertTrue(isset($courses[$course2->id])); $this->assertFalse(isset($courses[$course3->id])); // User u2 rated u3. $ratingquery = provider::get_sql_join('r', 'user', 'user', 'u.id', $u2->id); $sql = "SELECT u.id FROM {user} u {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $users = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(1, $users); $this->assertFalse(isset($users[$u1->id])); $this->assertFalse(isset($users[$u2->id])); $this->assertTrue(isset($users[$u3->id])); // Return any course which the u3 has rated. // User u3 did not rate anything. $ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u3->id); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(0, $courses); $this->assertFalse(isset($courses[$course1->id])); $this->assertFalse(isset($courses[$course2->id])); $this->assertFalse(isset($courses[$course3->id])); } /** * Ensure that the get_sql_join function returns valid SQL which returns the correct list of rated itemids. * This makes use of the optional inner join argument. */ public function test_get_sql_join_inner(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); // Rate the courses. $rm = new \rating_manager(); $ratingoptions = (object) [ 'component' => 'core_course', 'ratingarea' => 'course', 'scaleid' => 100, ]; // Rate all courses as u1, and something else in the same context. $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25); $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50); $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75); $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99); // Rate course2 as u2, and something else in a different context/component.. $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90); $this->rate_as_user($u2->id, 'user', 'user', $u3->id, \context_user::instance($u3->id), 10); // Return any course which the u1 has rated. // u1 rated all three courses. $ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u1->id, true); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(3, $courses); $this->assertTrue(isset($courses[$course1->id])); $this->assertTrue(isset($courses[$course2->id])); $this->assertTrue(isset($courses[$course3->id])); // User u1 rated files in course 3 only. $ratingquery = provider::get_sql_join('r', 'core_course', 'files', 'c.id', $u1->id, true); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(1, $courses); $this->assertFalse(isset($courses[$course1->id])); $this->assertFalse(isset($courses[$course2->id])); $this->assertTrue(isset($courses[$course3->id])); // Return any course which the u2 has rated. // User u2 rated only course 2. $ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u2->id, true); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(1, $courses); $this->assertFalse(isset($courses[$course1->id])); $this->assertTrue(isset($courses[$course2->id])); $this->assertFalse(isset($courses[$course3->id])); // User u2 rated u3. $ratingquery = provider::get_sql_join('r', 'user', 'user', 'u.id', $u2->id, true); $sql = "SELECT u.id FROM {user} u {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $users = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(1, $users); $this->assertFalse(isset($users[$u1->id])); $this->assertFalse(isset($users[$u2->id])); $this->assertTrue(isset($users[$u3->id])); // Return any course which the u3 has rated. // User u3 did not rate anything. $ratingquery = provider::get_sql_join('r', 'core_course', 'course', 'c.id', $u3->id, true); $sql = "SELECT c.id FROM {course} c {$ratingquery->join} WHERE {$ratingquery->userwhere}"; $courses = $DB->get_records_sql($sql, $ratingquery->params); $this->assertCount(0, $courses); $this->assertFalse(isset($courses[$course1->id])); $this->assertFalse(isset($courses[$course2->id])); $this->assertFalse(isset($courses[$course3->id])); } /** * Ensure that export_area_ratings exports all ratings that a user has made, and all ratings for a users own content. */ public function test_export_area_ratings(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); // Rate the courses. $rm = new \rating_manager(); $ratingoptions = (object) [ 'component' => 'core_course', 'ratingarea' => 'course', 'scaleid' => 100, ]; // Rate all courses as u1, and something else in the same context. $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25); $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50); $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75); $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99); $this->rate_as_user($u1->id, 'user', 'user', $u3->id, \context_user::instance($u3->id), 10); // Rate course2 as u2, and something else in a different context/component.. $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90); $this->rate_as_user($u2->id, 'user', 'user', $u3->id, \context_user::instance($u3->id), 20); // Test exports. // User 1 rated all three courses, and the core_course, and user 3. // User 1::course1 is stored in [] subcontext. $context = \context_course::instance($course1->id); $subcontext = []; provider::export_area_ratings($u1->id, $context, $subcontext, 'core_course', 'course', $course1->id, true); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $rating = $writer->get_related_data($subcontext, 'rating'); $this->assert_has_rating($u1, 25, $rating); // User 1::course2 is stored in ['foo'] subcontext. $context = \context_course::instance($course2->id); $subcontext = ['foo']; provider::export_area_ratings($u1->id, $context, $subcontext, 'core_course', 'course', $course2->id, true); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $result = $writer->get_related_data($subcontext, 'rating'); $this->assertCount(1, $result); $this->assert_has_rating($u1, 50, $result); // User 1::course3 is stored in ['foo'] subcontext. $context = \context_course::instance($course3->id); $subcontext = ['foo']; provider::export_area_ratings($u1->id, $context, $subcontext, 'core_course', 'course', $course3->id, true); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $result = $writer->get_related_data($subcontext, 'rating'); $this->assertCount(1, $result); $this->assert_has_rating($u1, 75, $result); // User 1::course3::files is stored in ['foo', 'files'] subcontext. $context = \context_course::instance($course3->id); $subcontext = ['foo', 'files']; provider::export_area_ratings($u1->id, $context, $subcontext, 'core_course', 'files', $course3->id, true); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $result = $writer->get_related_data($subcontext, 'rating'); $this->assertCount(1, $result); $this->assert_has_rating($u1, 99, $result); // Both users 1 and 2 rated user 3. // Exporting the data for user 3 should include both of those ratings. $context = \context_user::instance($u3->id); $subcontext = ['user']; provider::export_area_ratings($u3->id, $context, $subcontext, 'user', 'user', $u3->id, false); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $result = $writer->get_related_data($subcontext, 'rating'); $this->assertCount(2, $result); $this->assert_has_rating($u1, 10, $result); $this->assert_has_rating($u2, 20, $result); } /** * Test delete_ratings() method. */ public function test_delete_ratings(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); // Rate all courses as u1, and something else in the same context. $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25); $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50); $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75); $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99); $this->rate_as_user($u1->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 10); // Rate course2 as u2, and something else in a different context/component.. $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90); $this->rate_as_user($u2->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 20); // Delete all ratings in course1. $expectedratingscount = $DB->count_records('rating'); provider::delete_ratings(\context_course::instance($course1->id)); $expectedratingscount -= 1; $this->assertEquals($expectedratingscount, $DB->count_records('rating')); // Delete ratings in course2 specifying wrong component. provider::delete_ratings(\context_course::instance($course2->id), 'other_component'); $this->assertEquals($expectedratingscount, $DB->count_records('rating')); // Delete ratings in course2 specifying correct component. provider::delete_ratings(\context_course::instance($course2->id), 'core_course'); $expectedratingscount -= 2; $this->assertEquals($expectedratingscount, $DB->count_records('rating')); // Delete user ratings specifyng all attributes. provider::delete_ratings(\context_user::instance($u3->id), 'core_user', 'user', $u3->id); $expectedratingscount -= 2; $this->assertEquals($expectedratingscount, $DB->count_records('rating')); } /** * Test delete_ratings_select() method. */ public function test_delete_ratings_select(): void { global $DB; $this->resetAfterTest(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $u1 = $this->getDataGenerator()->create_user(); $u2 = $this->getDataGenerator()->create_user(); $u3 = $this->getDataGenerator()->create_user(); // Rate all courses as u1, and something else in the same context. $this->rate_as_user($u1->id, 'core_course', 'course', $course1->id, \context_course::instance($course1->id), 25); $this->rate_as_user($u1->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 50); $this->rate_as_user($u1->id, 'core_course', 'course', $course3->id, \context_course::instance($course3->id), 75); $this->rate_as_user($u1->id, 'core_course', 'files', $course3->id, \context_course::instance($course3->id), 99); $this->rate_as_user($u1->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 10); // Rate course2 as u2, and something else in a different context/component.. $this->rate_as_user($u2->id, 'core_course', 'course', $course2->id, \context_course::instance($course2->id), 90); $this->rate_as_user($u2->id, 'core_user', 'user', $u3->id, \context_user::instance($u3->id), 20); // Delete ratings in course1. list($sql, $params) = $DB->get_in_or_equal([$course1->id, $course2->id], SQL_PARAMS_NAMED); $expectedratingscount = $DB->count_records('rating'); provider::delete_ratings_select(\context_course::instance($course1->id), 'core_course', 'course', $sql, $params); $expectedratingscount -= 1; $this->assertEquals($expectedratingscount, $DB->count_records('rating')); } /** * Assert that a user has the correct rating. * * @param \stdClass $author The user with the rating * @param int $score The rating that was given * @param \stdClass[] The ratings which were found */ protected function assert_has_rating($author, $score, $actual) { $found = false; foreach ($actual as $rating) { if ($author->id == $rating->author) { $found = true; $this->assertEquals($score, $rating->rating); } } $this->assertTrue($found); } } tests/externallib_test.php000064400000027655152265255740012023 0ustar00. /** * External rating functions unit tests * * @package core_rating * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_rating; use core_external\external_api; use core_rating_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/rating/lib.php'); /** * External rating functions unit tests * * @package core_rating * @category external * @copyright 2015 Costantino Cito * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class externallib_test extends externallib_advanced_testcase { /** @var \stdClass course record. */ protected $course; /** @var \stdClass user record. */ protected $student1; /** @var \stdClass user record. */ protected $teacher1; /** @var \stdClass user record. */ protected $student2; /** @var \stdClass user record. */ protected $teacher2; /** @var \stdClass user record. */ protected $student3; /** @var \stdClass user record. */ protected $teacher3; /** @var \stdClass activity record. */ protected $forum; /** @var \stdClass activity record. */ protected $discussion; /** @var int context instance ID. */ protected $contextid; /** @var \stdClass forum post. */ protected $post; /** @var \stdClass a fieldset object, false or exception if error not found. */ protected $studentrole; /** @var \stdClass a fieldset object, false or exception if error not found. */ protected $teacherrole; /* * Set up for every test */ public function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); $this->course = self::getDataGenerator()->create_course(); $this->student1 = $this->getDataGenerator()->create_user(); $this->student2 = $this->getDataGenerator()->create_user(); $this->teacher1 = $this->getDataGenerator()->create_user(); $this->teacher2 = $this->getDataGenerator()->create_user(); $this->teacher3 = $this->getDataGenerator()->create_user(); $this->studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); unassign_capability('moodle/site:accessallgroups', $this->teacherrole->id); $this->getDataGenerator()->enrol_user($this->student1->id, $this->course->id, $this->studentrole->id); $this->getDataGenerator()->enrol_user($this->student2->id, $this->course->id, $this->studentrole->id); $this->getDataGenerator()->enrol_user($this->teacher1->id, $this->course->id, $this->teacherrole->id); $this->getDataGenerator()->enrol_user($this->teacher2->id, $this->course->id, $this->teacherrole->id); $this->getDataGenerator()->enrol_user($this->teacher3->id, $this->course->id, $this->teacherrole->id); // Create the forum. $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $this->course->id; // Set Aggregate type = Average of ratings. $record->assessed = RATING_AGGREGATE_AVERAGE; $record->scale = 100; $this->forum = self::getDataGenerator()->create_module('forum', $record); $this->contextid = \context_module::instance($this->forum->cmid)->id; // Add discussion to the forums. $record = new \stdClass(); $record->course = $this->course->id; $record->userid = $this->student1->id; $record->forum = $this->forum->id; $this->discussion = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); // Retrieve the first post. $this->post = $DB->get_record('forum_posts', array('discussion' => $this->discussion->id)); } /** * Test get_item_ratings */ public function test_get_item_ratings(): void { global $DB; // Rete the discussion as teacher1. $rating1 = new \stdClass(); $rating1->contextid = $this->contextid; $rating1->component = 'mod_forum'; $rating1->ratingarea = 'post'; $rating1->itemid = $this->post->id; $rating1->rating = 90; $rating1->scaleid = 100; $rating1->userid = $this->teacher1->id; $rating1->timecreated = time(); $rating1->timemodified = time(); $rating1->id = $DB->insert_record('rating', $rating1); // Rete the discussion as teacher2. $rating2 = new \stdClass(); $rating2->contextid = $this->contextid; $rating2->component = 'mod_forum'; $rating2->ratingarea = 'post'; $rating2->itemid = $this->post->id; $rating2->rating = 95; $rating2->scaleid = 100; $rating2->userid = $this->teacher2->id; $rating2->timecreated = time() + 1; $rating2->timemodified = time() + 1; $rating2->id = $DB->insert_record('rating', $rating2); // Delete teacher2, we must still receive the ratings. delete_user($this->teacher2); // Teachers can see all the ratings. $this->setUser($this->teacher1); $ratings = core_rating_external::get_item_ratings('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, ''); // We need to execute the return values cleaning process to simulate the web service server. $ratings = external_api::clean_returnvalue(core_rating_external::get_item_ratings_returns(), $ratings); $this->assertCount(2, $ratings['ratings']); $indexedratings = array(); foreach ($ratings['ratings'] as $rating) { $indexedratings[$rating['id']] = $rating; } $this->assertEquals($rating1->rating.' / '.$rating1->scaleid, $indexedratings[$rating1->id]['rating']); $this->assertEquals($rating2->rating.' / '.$rating2->scaleid, $indexedratings[$rating2->id]['rating']); $this->assertEquals($rating1->userid, $indexedratings[$rating1->id]['userid']); $this->assertEquals($rating2->userid, $indexedratings[$rating2->id]['userid']); // Student can see ratings. $this->setUser($this->student1); $ratings = core_rating_external::get_item_ratings('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, ''); // We need to execute the return values cleaning process to simulate the web service server. $ratings = external_api::clean_returnvalue(core_rating_external::get_item_ratings_returns(), $ratings); $this->assertCount(2, $ratings['ratings']); // Invalid item. try { $ratings = core_rating_external::get_item_ratings('module', $this->forum->cmid, 'mod_forum', 'post', 0, 100, ''); $this->fail('Exception expected due invalid itemid.'); } catch (\moodle_exception $e) { $this->assertEquals('invalidrecord', $e->errorcode); } // Invalid area. try { $ratings = core_rating_external::get_item_ratings('module', $this->forum->cmid, 'mod_forum', 'xyz', $this->post->id, 100, ''); $this->fail('Exception expected due invalid rating area.'); } catch (\moodle_exception $e) { $this->assertEquals('invalidratingarea', $e->errorcode); } // Invalid context. invalid_parameter_exception. try { $ratings = core_rating_external::get_item_ratings('module', 0, 'mod_forum', 'post', $this->post->id, 100, ''); $this->fail('Exception expected due invalid context.'); } catch (\invalid_parameter_exception $e) { $this->assertEquals('invalidparameter', $e->errorcode); } // Test for groupmode. set_coursemodule_groupmode($this->forum->cmid, SEPARATEGROUPS); $group = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id)); groups_add_member($group, $this->teacher1); $this->discussion->groupid = $group->id; $DB->update_record('forum_discussions', $this->discussion); // Error for teacher3 and 2 ratings for teacher1 should be returned. $this->setUser($this->teacher1); $ratings = core_rating_external::get_item_ratings('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, ''); // We need to execute the return values cleaning process to simulate the web service server. $ratings = external_api::clean_returnvalue(core_rating_external::get_item_ratings_returns(), $ratings); $this->assertCount(2, $ratings['ratings']); $this->setUser($this->teacher3); try { $ratings = core_rating_external::get_item_ratings('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, ''); $this->fail('Exception expected due invalid group permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('noviewrate', $e->errorcode); } } /** * Test add_rating */ public function test_add_rating(): void { $this->setUser($this->teacher1); // First rating of 50. $rating = core_rating_external::add_rating('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, 50, $this->student1->id, RATING_AGGREGATE_AVERAGE); // We need to execute the return values cleaning process to simulate the web service server. $rating = external_api::clean_returnvalue(core_rating_external::add_rating_returns(), $rating); $this->assertTrue($rating['success']); $this->assertEquals(50, $rating['aggregate']); $this->assertEquals(1, $rating['count']); // New different rate (it will replace the existing one). $rating = core_rating_external::add_rating('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, 100, $this->student1->id, RATING_AGGREGATE_AVERAGE); $rating = external_api::clean_returnvalue(core_rating_external::add_rating_returns(), $rating); $this->assertTrue($rating['success']); $this->assertEquals(100, $rating['aggregate']); $this->assertEquals(1, $rating['count']); // Rate as other user. $this->setUser($this->teacher2); $rating = core_rating_external::add_rating('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, 50, $this->student1->id, RATING_AGGREGATE_AVERAGE); $rating = external_api::clean_returnvalue(core_rating_external::add_rating_returns(), $rating); $this->assertEquals(75, $rating['aggregate']); $this->assertEquals(2, $rating['count']); // Try to rate my own post. $this->setUser($this->student1); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('ratepermissiondenied', 'rating')); $rating = core_rating_external::add_rating('module', $this->forum->cmid, 'mod_forum', 'post', $this->post->id, 100, 100, $this->student1->id, RATING_AGGREGATE_AVERAGE); } }