2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * A class representing a single rating and containing some static methods for manipulating ratings
20 * @package core_rating
22 * @copyright 2010 Andrew Davis
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 define('RATING_UNSET_RATING', -999);
28 define ('RATING_AGGREGATE_NONE', 0); // No ratings.
29 define ('RATING_AGGREGATE_AVERAGE', 1);
30 define ('RATING_AGGREGATE_COUNT', 2);
31 define ('RATING_AGGREGATE_MAXIMUM', 3);
32 define ('RATING_AGGREGATE_MINIMUM', 4);
33 define ('RATING_AGGREGATE_SUM', 5);
35 define ('RATING_DEFAULT_SCALE', 5);
38 * The rating class represents a single rating by a single user
40 * @package core_rating
42 * @copyright 2010 Andrew Davis
43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 class rating
implements renderable
{
49 * @var stdClass The context in which this rating exists
54 * @var string The component using ratings. For example "mod_forum"
59 * @var string The rating area to associate this rating with
60 * This allows a plugin to rate more than one thing by specifying different rating areas
62 public $ratingarea = null;
65 * @var int The id of the item (forum post, glossary item etc) being rated
70 * @var int The id scale (1-5, 0-100) that was in use when the rating was submitted
75 * @var int The id of the user who submitted the rating
80 * @var stdclass settings for this rating. Necessary to render the rating.
85 * @var int The Id of this rating within the rating table. This is only set if the rating already exists
90 * @var int The aggregate of the combined ratings for the associated item. This is only set if the rating already exists
92 public $aggregate = null;
95 * @var int The total number of ratings for the associated item. This is only set if the rating already exists
100 * @var int The rating the associated user gave the associated item. This is only set if the rating already exists
102 public $rating = null;
105 * @var int The time the associated item was created
107 public $itemtimecreated = null;
110 * @var int The id of the user who submitted the rating
112 public $itemuserid = null;
117 * @param stdClass $options {
118 * context => context context to use for the rating [required]
119 * component => component using ratings ie mod_forum [required]
120 * ratingarea => ratingarea to associate this rating with [required]
121 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
122 * scaleid => int The scale in use when the rating was submitted [required]
123 * userid => int The id of the user who submitted the rating [required]
124 * settings => Settings for the rating object [optional]
125 * id => The id of this rating (if the rating is from the db) [optional]
126 * aggregate => The aggregate for the rating [optional]
127 * count => The number of ratings [optional]
128 * rating => The rating given by the user [optional]
131 public function __construct($options) {
132 $this->context
= $options->context
;
133 $this->component
= $options->component
;
134 $this->ratingarea
= $options->ratingarea
;
135 $this->itemid
= $options->itemid
;
136 $this->scaleid
= $options->scaleid
;
137 $this->userid
= $options->userid
;
139 if (isset($options->settings
)) {
140 $this->settings
= $options->settings
;
142 if (isset($options->id
)) {
143 $this->id
= $options->id
;
145 if (isset($options->aggregate
)) {
146 $this->aggregate
= $options->aggregate
;
148 if (isset($options->count
)) {
149 $this->count
= $options->count
;
151 if (isset($options->rating
)) {
152 $this->rating
= $options->rating
;
157 * Update this rating in the database
159 * @param int $rating the integer value of this rating
161 public function update_rating($rating) {
166 $data = new stdClass
;
167 $data->rating
= $rating;
168 $data->timemodified
= $time;
170 $item = new stdclass();
171 $item->id
= $this->itemid
;
172 $items = array($item);
174 $ratingoptions = new stdClass
;
175 $ratingoptions->context
= $this->context
;
176 $ratingoptions->component
= $this->component
;
177 $ratingoptions->ratingarea
= $this->ratingarea
;
178 $ratingoptions->items
= $items;
179 $ratingoptions->aggregate
= RATING_AGGREGATE_AVERAGE
; // We dont actually care what aggregation method is applied.
180 $ratingoptions->scaleid
= $this->scaleid
;
181 $ratingoptions->userid
= $this->userid
;
183 $rm = new rating_manager();
184 $items = $rm->get_ratings($ratingoptions);
185 $firstitem = $items[0]->rating
;
187 if (empty($firstitem->id
)) {
188 // Insert a new rating.
189 $data->contextid
= $this->context
->id
;
190 $data->component
= $this->component
;
191 $data->ratingarea
= $this->ratingarea
;
192 $data->rating
= $rating;
193 $data->scaleid
= $this->scaleid
;
194 $data->userid
= $this->userid
;
195 $data->itemid
= $this->itemid
;
196 $data->timecreated
= $time;
197 $data->timemodified
= $time;
198 $DB->insert_record('rating', $data);
200 // Update the rating.
201 $data->id
= $firstitem->id
;
202 $DB->update_record('rating', $data);
207 * Retreive the integer value of this rating
209 * @return int the integer value of this rating object
211 public function get_rating() {
212 return $this->rating
;
216 * Returns this ratings aggregate value as a string.
218 * @return string ratings aggregate value
220 public function get_aggregate_string() {
222 $aggregate = $this->aggregate
;
223 $method = $this->settings
->aggregationmethod
;
225 // Only display aggregate if aggregation method isn't COUNT.
227 if (is_numeric($aggregate) && $method != RATING_AGGREGATE_COUNT
) {
228 if ($method != RATING_AGGREGATE_SUM
&& !$this->settings
->scale
->isnumeric
) {
230 // Round aggregate as we're using it as an index.
231 $aggregatestr .= $this->settings
->scale
->scaleitems
[round($aggregate)];
232 } else { // Aggregation is SUM or the scale is numeric.
233 $aggregatestr .= round($aggregate, 1);
237 return $aggregatestr;
241 * Returns true if the user is able to rate this rating object
243 * @param int $userid Current user assumed if left empty
244 * @return bool true if the user is able to rate this rating object
246 public function user_can_rate($userid = null) {
247 if (empty($userid)) {
251 // You can't rate your item.
252 if ($this->itemuserid
== $userid) {
255 // You can't rate if you don't have the system cap.
256 if (!$this->settings
->permissions
->rate
) {
259 // You can't rate if you don't have the plugin cap.
260 if (!$this->settings
->pluginpermissions
->rate
) {
264 // You can't rate if the item was outside of the assessment times.
265 $timestart = $this->settings
->assesstimestart
;
266 $timefinish = $this->settings
->assesstimefinish
;
267 $timecreated = $this->itemtimecreated
;
268 if (!empty($timestart) && !empty($timefinish) && ($timecreated < $timestart ||
$timecreated > $timefinish)) {
275 * Returns true if the user is able to view the aggregate for this rating object.
277 * @param int|null $userid If left empty the current user is assumed.
278 * @return bool true if the user is able to view the aggregate for this rating object
280 public function user_can_view_aggregate($userid = null) {
281 if (empty($userid)) {
286 // 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.
287 // Note that viewany doesnt mean you can see the aggregate or ratings of your own items.
288 if ((empty($this->itemuserid
) or $this->itemuserid
!= $userid)
289 && $this->settings
->permissions
->viewany
290 && $this->settings
->pluginpermissions
->viewany
) {
295 // If its the current user's item and they have permission to view the aggregate on their own items.
296 if ($this->itemuserid
== $userid
297 && $this->settings
->permissions
->view
298 && $this->settings
->pluginpermissions
->view
) {
307 * Returns a URL to view all of the ratings for the item this rating is for.
309 * 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
310 * (this one included).
312 * @param bool $popup whether of not the URL should be loaded in a popup
313 * @return moodle_url URL to view all of the ratings for the item this rating is for.
315 public function get_view_ratings_url($popup = false) {
317 'contextid' => $this->context
->id
,
318 'component' => $this->component
,
319 'ratingarea' => $this->ratingarea
,
320 'itemid' => $this->itemid
,
321 'scaleid' => $this->settings
->scale
->id
324 $attributes['popup'] = 1;
326 return new moodle_url('/rating/index.php', $attributes);
330 * Returns a URL that can be used to rate the associated item.
332 * @param int|null $rating The rating to give the item, if null then no rating param is added.
333 * @param moodle_url|string $returnurl The URL to return to.
334 * @return moodle_url can be used to rate the associated item.
336 public function get_rate_url($rating = null, $returnurl = null) {
337 if (empty($returnurl)) {
338 if (!empty($this->settings
->returnurl
)) {
339 $returnurl = $this->settings
->returnurl
;
342 $returnurl = $PAGE->url
;
346 'contextid' => $this->context
->id
,
347 'component' => $this->component
,
348 'ratingarea' => $this->ratingarea
,
349 'itemid' => $this->itemid
,
350 'scaleid' => $this->settings
->scale
->id
,
351 'returnurl' => $returnurl,
352 'rateduserid' => $this->itemuserid
,
353 'aggregation' => $this->settings
->aggregationmethod
,
354 'sesskey' => sesskey()
356 if (!empty($rating)) {
357 $args['rating'] = $rating;
359 $url = new moodle_url('/rating/rate.php', $args);
363 } // End rating class definition.
366 * The rating_manager class provides the ability to retrieve sets of ratings from the database
368 * @package core_rating
370 * @copyright 2010 Andrew Davis
371 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
374 class rating_manager
{
377 * @var array An array of calculated scale options to save us generating them for each request.
379 protected $scales = array();
382 * Delete one or more ratings. Specify either a rating id, an item id or just the context id.
384 * @global moodle_database $DB
385 * @param stdClass $options {
386 * contextid => int the context in which the ratings exist [required]
387 * ratingid => int the id of an individual rating to delete [optional]
388 * userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional]
389 * itemid => int delete all ratings attached to this item [optional]
390 * component => string The component to delete ratings from [optional]
391 * ratingarea => string The ratingarea to delete ratings from [optional]
394 public function delete_ratings($options) {
397 if (empty($options->contextid
)) {
398 throw new coding_exception('The context option is a required option when deleting ratings.');
401 $conditions = array('contextid' => $options->contextid
);
402 $possibleconditions = array(
404 'userid' => 'userid',
405 'itemid' => 'itemid',
406 'component' => 'component',
407 'ratingarea' => 'ratingarea'
409 foreach ($possibleconditions as $option => $field) {
410 if (isset($options->{$option})) {
411 $conditions[$field] = $options->{$option};
414 $DB->delete_records('rating', $conditions);
418 * Returns an array of ratings for a given item (forum post, glossary entry etc).
420 * This returns all users ratings for a single item
422 * @param stdClass $options {
423 * context => context the context in which the ratings exists [required]
424 * component => component using ratings ie mod_forum [required]
425 * ratingarea => ratingarea to associate this rating with [required]
426 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
427 * sort => string SQL sort by clause [optional]
429 * @return array an array of ratings
431 public function get_all_ratings_for_item($options) {
434 if (!isset($options->context
)) {
435 throw new coding_exception('The context option is a required option when getting ratings for an item.');
437 if (!isset($options->itemid
)) {
438 throw new coding_exception('The itemid option is a required option when getting ratings for an item.');
440 if (!isset($options->component
)) {
441 throw new coding_exception('The component option is now a required option when getting ratings for an item.');
443 if (!isset($options->ratingarea
)) {
444 throw new coding_exception('The ratingarea option is now a required option when getting ratings for an item.');
448 if (!empty($options->sort
)) {
449 $sortclause = "ORDER BY $options->sort";
453 'contextid' => $options->context
->id
,
454 'itemid' => $options->itemid
,
455 'component' => $options->component
,
456 'ratingarea' => $options->ratingarea
,
458 $userfieldsapi = \core_user\fields
::for_userpic();
459 $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects
;
460 $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, r.component, r.ratingarea, $userfields
462 LEFT JOIN {user} u ON r.userid = u.id
463 WHERE r.contextid = :contextid AND
464 r.itemid = :itemid AND
465 r.component = :component AND
466 r.ratingarea = :ratingarea
469 return $DB->get_records_sql($sql, $params);
473 * Adds rating objects to an array of items (forum posts, glossary entries etc). Rating objects are available at $item->rating
475 * @param stdClass $options {
476 * context => context the context in which the ratings exists [required]
477 * component => the component name ie mod_forum [required]
478 * ratingarea => the ratingarea we are interested in [required]
479 * items => array items like forum posts or glossary items. Each item needs an 'id' ie $items[0]->id [required]
480 * aggregate => int aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
481 * scaleid => int the scale from which the user can select a rating [required]
482 * userid => int the id of the current user [optional]
483 * returnurl => string the url to return the user to after submitting a rating. Null for ajax requests [optional]
484 * assesstimestart => int only allow rating of items created after this timestamp [optional]
485 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
486 * @return array the array of items with their ratings attached at $items[0]->rating
488 public function get_ratings($options) {
491 if (!isset($options->context
)) {
492 throw new coding_exception('The context option is a required option when getting ratings.');
495 if (!isset($options->component
)) {
496 throw new coding_exception('The component option is a required option when getting ratings.');
499 if (!isset($options->ratingarea
)) {
500 throw new coding_exception('The ratingarea option is a required option when getting ratings.');
503 if (!isset($options->scaleid
)) {
504 throw new coding_exception('The scaleid option is a required option when getting ratings.');
507 if (!isset($options->items
)) {
508 throw new coding_exception('The items option is a required option when getting ratings.');
509 } else if (empty($options->items
)) {
513 if (!isset($options->aggregate
)) {
514 throw new coding_exception('The aggregate option is a required option when getting ratings.');
515 } else if ($options->aggregate
== RATING_AGGREGATE_NONE
) {
516 // Ratings are not enabled.
517 return $options->items
;
519 $aggregatestr = $this->get_aggregation_method($options->aggregate
);
521 // Default the userid to the current user if it is not set.
522 if (empty($options->userid
)) {
525 $userid = $options->userid
;
528 // Get the item table name, the item id field, and the item user field for the given rating item
529 // from the related component.
530 list($type, $name) = core_component
::normalize_component($options->component
);
531 $default = array(null, 'id', 'userid');
532 list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type,
539 // Create an array of item IDs.
541 foreach ($options->items
as $item) {
542 $itemids[] = $item->{$itemidcol};
545 // Get the items from the database.
546 list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED
);
547 $params['contextid'] = $options->context
->id
;
548 $params['userid'] = $userid;
549 $params['component'] = $options->component
;
550 $params['ratingarea'] = $options->ratingarea
;
552 $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating
554 WHERE r.userid = :userid AND
555 r.contextid = :contextid AND
556 r.itemid {$itemidtest} AND
557 r.component = :component AND
558 r.ratingarea = :ratingarea
560 $userratings = $DB->get_records_sql($sql, $params);
562 $sql = "SELECT r.itemid, $aggregatestr(r.rating) AS aggrrating, COUNT(r.rating) AS numratings
564 WHERE r.contextid = :contextid AND
565 r.itemid {$itemidtest} AND
566 r.component = :component AND
567 r.ratingarea = :ratingarea
568 GROUP BY r.itemid, r.component, r.ratingarea, r.contextid
570 $aggregateratings = $DB->get_records_sql($sql, $params);
572 $ratingoptions = new stdClass
;
573 $ratingoptions->context
= $options->context
;
574 $ratingoptions->component
= $options->component
;
575 $ratingoptions->ratingarea
= $options->ratingarea
;
576 $ratingoptions->settings
= $this->generate_rating_settings_object($options);
577 foreach ($options->items
as $item) {
578 $founduserrating = false;
579 foreach ($userratings as $userrating) {
580 // Look for an existing rating from this user of this item.
581 if ($item->{$itemidcol} == $userrating->itemid
) {
582 // Note: rec->scaleid = the id of scale at the time the rating was submitted.
583 // It may be different from the current scale id.
584 $ratingoptions->scaleid
= $userrating->scaleid
;
585 $ratingoptions->userid
= $userrating->userid
;
586 $ratingoptions->id
= $userrating->id
;
587 $ratingoptions->rating
= min($userrating->usersrating
, $ratingoptions->settings
->scale
->max
);
589 $founduserrating = true;
593 if (!$founduserrating) {
594 $ratingoptions->scaleid
= null;
595 $ratingoptions->userid
= null;
596 $ratingoptions->id
= null;
597 $ratingoptions->rating
= null;
600 if (array_key_exists($item->{$itemidcol}, $aggregateratings)) {
601 $rec = $aggregateratings[$item->{$itemidcol}];
602 $ratingoptions->itemid
= $item->{$itemidcol};
603 $ratingoptions->aggregate
= min($rec->aggrrating
, $ratingoptions->settings
->scale
->max
);
604 $ratingoptions->count
= $rec->numratings
;
606 $ratingoptions->itemid
= $item->{$itemidcol};
607 $ratingoptions->aggregate
= null;
608 $ratingoptions->count
= 0;
611 $rating = new rating($ratingoptions);
612 $rating->itemtimecreated
= $this->get_item_time_created($item);
613 if (!empty($item->{$itemuseridcol})) {
614 $rating->itemuserid
= $item->{$itemuseridcol};
616 $item->rating
= $rating;
619 return $options->items
;
623 * Generates a rating settings object based upon the options it is provided.
625 * @param stdClass $options {
626 * context => context the context in which the ratings exists [required]
627 * component => string The component the items belong to [required]
628 * ratingarea => string The ratingarea the items belong to [required]
629 * aggregate => int Aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
630 * scaleid => int the scale from which the user can select a rating [required]
631 * returnurl => string the url to return the user to after submitting a rating. Null for ajax requests [optional]
632 * assesstimestart => int only allow rating of items created after this timestamp [optional]
633 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
634 * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional]
635 * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional]
637 * @return stdClass rating settings object
639 protected function generate_rating_settings_object($options) {
641 if (!isset($options->context
)) {
642 throw new coding_exception('The context option is a required option when generating a rating settings object.');
644 if (!isset($options->component
)) {
645 throw new coding_exception('The component option is now a required option when generating a rating settings object.');
647 if (!isset($options->ratingarea
)) {
648 throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.');
650 if (!isset($options->aggregate
)) {
651 throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.');
653 if (!isset($options->scaleid
)) {
654 throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.');
657 // Settings that are common to all ratings objects in this context.
658 $settings = new stdClass
;
659 $settings->scale
= $this->generate_rating_scale_object($options->scaleid
); // The scale to use now.
660 $settings->aggregationmethod
= $options->aggregate
;
661 $settings->assesstimestart
= null;
662 $settings->assesstimefinish
= null;
664 // Collect options into the settings object.
665 if (!empty($options->assesstimestart
)) {
666 $settings->assesstimestart
= $options->assesstimestart
;
668 if (!empty($options->assesstimefinish
)) {
669 $settings->assesstimefinish
= $options->assesstimefinish
;
671 if (!empty($options->returnurl
)) {
672 $settings->returnurl
= $options->returnurl
;
675 // Check site capabilities.
676 $settings->permissions
= new stdClass
;
677 // Can view the aggregate of ratings of their own items.
678 $settings->permissions
->view
= has_capability('moodle/rating:view', $options->context
);
679 // Can view the aggregate of ratings of other people's items.
680 $settings->permissions
->viewany
= has_capability('moodle/rating:viewany', $options->context
);
681 // Can view individual ratings.
682 $settings->permissions
->viewall
= has_capability('moodle/rating:viewall', $options->context
);
683 // Can submit ratings.
684 $settings->permissions
->rate
= has_capability('moodle/rating:rate', $options->context
);
686 // Check module capabilities
687 // This is mostly for backwards compatability with old modules that previously implemented their own ratings.
688 $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context
->id
,
690 $options->ratingarea
);
691 $settings->pluginpermissions
= new stdClass
;
692 $settings->pluginpermissions
->view
= $pluginpermissionsarray['view'];
693 $settings->pluginpermissions
->viewany
= $pluginpermissionsarray['viewany'];
694 $settings->pluginpermissions
->viewall
= $pluginpermissionsarray['viewall'];
695 $settings->pluginpermissions
->rate
= $pluginpermissionsarray['rate'];
701 * Generates a scale object that can be returned
703 * @global moodle_database $DB moodle database object
704 * @param int $scaleid scale-type identifier
705 * @return stdClass scale for ratings
707 protected function generate_rating_scale_object($scaleid) {
709 if (!array_key_exists('s'.$scaleid, $this->scales
)) {
710 $scale = new stdClass
;
711 $scale->id
= $scaleid;
713 $scale->courseid
= null;
714 $scale->scaleitems
= array();
715 $scale->isnumeric
= true;
716 $scale->max
= $scaleid;
719 // It is a proper scale (not numeric).
720 $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid)));
722 // We need to generate an array with string keys starting at 1.
723 $scalearray = explode(',', $scalerecord->scale
);
724 $c = count($scalearray);
725 for ($i = 0; $i < $c; $i++
) {
726 // Treat index as a string to allow sorting without changing the value.
727 $scale->scaleitems
[(string)($i +
1)] = $scalearray[$i];
729 krsort($scale->scaleitems
); // Have the highest grade scale item appear first.
730 $scale->isnumeric
= false;
731 $scale->name
= $scalerecord->name
;
732 $scale->courseid
= $scalerecord->courseid
;
733 $scale->max
= count($scale->scaleitems
);
736 // Generate an array of values for numeric scales.
737 for ($i = 0; $i <= (int)$scaleid; $i++
) {
738 $scale->scaleitems
[(string)$i] = $i;
741 $this->scales
['s'.$scaleid] = $scale;
743 return $this->scales
['s'.$scaleid];
747 * Gets the time the given item was created
749 * TODO: MDL-31511 - Find a better solution for this, its not ideal to test for fields really we should be
750 * asking the component the item belongs to what field to look for or even the value we
753 * @param stdClass $item
754 * @return int|null return null if the created time is unavailable, otherwise return a timestamp
756 protected function get_item_time_created($item) {
757 if (!empty($item->created
)) {
758 return $item->created
; // The forum_posts table has created instead of timecreated.
759 } else if (!empty($item->timecreated
)) {
760 return $item->timecreated
;
767 * Returns an array of grades calculated by aggregating item ratings.
769 * @param stdClass $options {
770 * userid => int the id of the user whose items were rated, NOT the user who submitted ratings. 0 to update all. [required]
771 * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
772 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
773 * itemtable => int the table containing the items [required]
774 * itemtableusercolum => int the column of the user table containing the item owner's user id [required]
775 * component => The component for the ratings [required]
776 * ratingarea => The ratingarea for the ratings [required]
777 * contextid => int the context in which the rated items exist [optional]
778 * modulename => string the name of the module [optional]
779 * moduleid => int the id of the module instance [optional]
781 * @return array the array of the user's grades
783 public function get_user_grades($options) {
788 if (!isset($options->component
)) {
789 throw new coding_exception('The component option is now a required option when getting user grades from ratings.');
791 if (!isset($options->ratingarea
)) {
792 throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.');
795 // If the calling code doesn't supply a context id we'll have to figure it out.
796 if (!empty($options->contextid
)) {
797 $contextid = $options->contextid
;
798 } else if (!empty($options->modulename
) && !empty($options->moduleid
)) {
799 $modulename = $options->modulename
;
800 $moduleid = intval($options->moduleid
);
802 // Going direct to the db for the context id seems wrong.
803 $ctxselect = ', ' . context_helper
::get_preload_record_columns_sql('ctx');
804 $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = cm.id AND ctx.contextlevel = :contextlevel)";
805 $sql = "SELECT cm.* $ctxselect
806 FROM {course_modules} cm
807 LEFT JOIN {modules} mo ON mo.id = cm.module
808 LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
809 WHERE mo.name=:modulename AND
811 $params = array('modulename' => $modulename, 'moduleid' => $moduleid, 'contextlevel' => CONTEXT_MODULE
);
812 $contextrecord = $DB->get_record_sql($sql, $params, '*', MUST_EXIST
);
813 $contextid = $contextrecord->ctxid
;
817 $params['contextid'] = $contextid;
818 $params['component'] = $options->component
;
819 $params['ratingarea'] = $options->ratingarea
;
820 $itemtable = $options->itemtable
;
821 $itemtableusercolumn = $options->itemtableusercolumn
;
822 $scaleid = $options->scaleid
;
823 $aggregationstring = $this->get_aggregation_method($options->aggregationmethod
);
825 // If userid is not 0 we only want the grade for a single user.
826 $singleuserwhere = '';
827 if ($options->userid
!= 0) {
828 $params['userid1'] = intval($options->userid
);
829 $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
832 // MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)".
833 // r.contextid will be null for users who haven't been rated yet.
834 // No longer including users who haven't been rated to reduce memory requirements.
835 $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
837 LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
838 LEFT JOIN {rating} r ON r.itemid=i.id
839 WHERE r.contextid = :contextid AND
840 r.component = :component AND
841 r.ratingarea = :ratingarea
844 $results = $DB->get_records_sql($sql, $params);
850 if ($options->scaleid
>= 0) {
852 $max = $options->scaleid
;
855 $scale = $DB->get_record('scale', array('id' => -$options->scaleid
));
857 $scale = explode(',', $scale->scale
);
858 $max = count($scale);
860 debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist');
864 // It could throw off the grading if count and sum returned a rawgrade higher than scale
865 // so to prevent it we review the results and ensure that rawgrade does not exceed the scale.
866 // If it does we set rawgrade = scale (i.e. full credit).
867 foreach ($results as $rid => $result) {
868 if ($options->scaleid
>= 0) {
870 if ($result->rawgrade
> $options->scaleid
) {
871 $results[$rid]->rawgrade
= $options->scaleid
;
875 if (!empty($scale) && $result->rawgrade
> $max) {
876 $results[$rid]->rawgrade
= $max;
886 * Returns array of aggregate types. Used by ratings.
888 * @return array aggregate types
890 public function get_aggregate_types() {
891 return array (RATING_AGGREGATE_NONE
=> get_string('aggregatenone', 'rating'),
892 RATING_AGGREGATE_AVERAGE
=> get_string('aggregateavg', 'rating'),
893 RATING_AGGREGATE_COUNT
=> get_string('aggregatecount', 'rating'),
894 RATING_AGGREGATE_MAXIMUM
=> get_string('aggregatemax', 'rating'),
895 RATING_AGGREGATE_MINIMUM
=> get_string('aggregatemin', 'rating'),
896 RATING_AGGREGATE_SUM
=> get_string('aggregatesum', 'rating'));
900 * Converts an aggregation method constant into something that can be included in SQL
902 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
903 * @return string an SQL aggregation method
905 public function get_aggregation_method($aggregate) {
906 $aggregatestr = null;
908 case RATING_AGGREGATE_AVERAGE
:
909 $aggregatestr = 'AVG';
911 case RATING_AGGREGATE_COUNT
:
912 $aggregatestr = 'COUNT';
914 case RATING_AGGREGATE_MAXIMUM
:
915 $aggregatestr = 'MAX';
917 case RATING_AGGREGATE_MINIMUM
:
918 $aggregatestr = 'MIN';
920 case RATING_AGGREGATE_SUM
:
921 $aggregatestr = 'SUM';
924 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270.
925 debugging('Incorrect call to get_aggregation_method(), incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER
);
927 return $aggregatestr;
931 * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated
933 * @param int $contextid The current context id
934 * @param string $component the name of the component that is using ratings ie 'mod_forum'
935 * @param string $ratingarea The area the rating is associated with
936 * @return array rating related permissions
938 public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
939 $pluginpermissionsarray = null;
941 $defaultpluginpermissions = array('rate' => false, 'view' => false, 'viewany' => false, 'viewall' => false);
942 if (!empty($component)) {
943 list($type, $name) = core_component
::normalize_component($component);
944 $pluginpermissionsarray = plugin_callback($type,
948 array($contextid, $component, $ratingarea),
949 $defaultpluginpermissions);
951 $pluginpermissionsarray = $defaultpluginpermissions;
953 return $pluginpermissionsarray;
957 * Validates a submitted rating
959 * @param array $params submitted data
960 * context => object the context in which the rated items exists [required]
961 * component => The component the rating belongs to [required]
962 * ratingarea => The ratingarea the rating is associated with [required]
963 * itemid => int the ID of the object being rated [required]
964 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
965 * rating => int the submitted rating
966 * rateduserid => int the id of the user whose items have been rated. 0 to update all. [required]
967 * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
968 * @return boolean true if the rating is valid, false if callback not found, throws rating_exception if rating is invalid
970 public function check_rating_is_valid($params) {
972 if (!isset($params['context'])) {
973 throw new coding_exception('The context option is a required option when checking rating validity.');
975 if (!isset($params['component'])) {
976 throw new coding_exception('The component option is now a required option when checking rating validity');
978 if (!isset($params['ratingarea'])) {
979 throw new coding_exception('The ratingarea option is now a required option when checking rating validity');
981 if (!isset($params['itemid'])) {
982 throw new coding_exception('The itemid option is now a required option when checking rating validity');
984 if (!isset($params['scaleid'])) {
985 throw new coding_exception('The scaleid option is now a required option when checking rating validity');
987 if (!isset($params['rateduserid'])) {
988 throw new coding_exception('The rateduserid option is now a required option when checking rating validity');
991 list($plugintype, $pluginname) = core_component
::normalize_component($params['component']);
993 // This looks for a function like forum_rating_validate() in mod_forum lib.php
994 // wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments.
995 $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
997 // If null then the callback does not exist.
998 if ($isvalid === null) {
1000 debugging('rating validation callback not found for component '. clean_param($component, PARAM_ALPHANUMEXT
));
1006 * Initialises JavaScript to enable AJAX ratings on the provided page
1008 * @param moodle_page $page
1009 * @return true always returns true
1011 public function initialise_rating_javascript(moodle_page
$page) {
1014 // Only needs to be initialized once.
1015 static $done = false;
1020 $page->requires
->js_init_call('M.core_rating.init');
1027 * Returns a string that describes the aggregation method that was provided.
1029 * @param string $aggregationmethod
1030 * @return string describes the aggregation method that was provided
1032 public function get_aggregate_label($aggregationmethod) {
1033 $aggregatelabel = '';
1034 switch ($aggregationmethod) {
1035 case RATING_AGGREGATE_AVERAGE
:
1036 $aggregatelabel .= get_string("aggregateavg", "rating");
1038 case RATING_AGGREGATE_COUNT
:
1039 $aggregatelabel .= get_string("aggregatecount", "rating");
1041 case RATING_AGGREGATE_MAXIMUM
:
1042 $aggregatelabel .= get_string("aggregatemax", "rating");
1044 case RATING_AGGREGATE_MINIMUM
:
1045 $aggregatelabel .= get_string("aggregatemin", "rating");
1047 case RATING_AGGREGATE_SUM
:
1048 $aggregatelabel .= get_string("aggregatesum", "rating");
1051 $aggregatelabel .= get_string('labelsep', 'langconfig');
1052 return $aggregatelabel;
1058 * @param stdClass $cm course module object
1059 * @param stdClass $context context object
1060 * @param string $component component name
1061 * @param string $ratingarea rating area
1062 * @param int $itemid the item id
1063 * @param int $scaleid the scale id
1064 * @param int $userrating the user rating
1065 * @param int $rateduserid the rated user id
1066 * @param int $aggregationmethod the aggregation method
1069 public function add_rating($cm, $context, $component, $ratingarea, $itemid, $scaleid, $userrating, $rateduserid,
1070 $aggregationmethod) {
1071 global $CFG, $DB, $USER;
1073 $result = new stdClass
;
1074 // Check the module rating permissions.
1075 // Doing this check here rather than within rating_manager::get_ratings() so we can return a error response.
1076 $pluginpermissionsarray = $this->get_plugin_permissions_array($context->id
, $component, $ratingarea);
1078 if (!$pluginpermissionsarray['rate']) {
1079 $result->error
= 'ratepermissiondenied';
1083 'context' => $context,
1084 'component' => $component,
1085 'ratingarea' => $ratingarea,
1086 'itemid' => $itemid,
1087 'scaleid' => $scaleid,
1088 'rating' => $userrating,
1089 'rateduserid' => $rateduserid,
1090 'aggregation' => $aggregationmethod
1092 if (!$this->check_rating_is_valid($params)) {
1093 $result->error
= 'ratinginvalid';
1098 // Rating options used to update the rating then retrieve the aggregate.
1099 $ratingoptions = new stdClass
;
1100 $ratingoptions->context
= $context;
1101 $ratingoptions->ratingarea
= $ratingarea;
1102 $ratingoptions->component
= $component;
1103 $ratingoptions->itemid
= $itemid;
1104 $ratingoptions->scaleid
= $scaleid;
1105 $ratingoptions->userid
= $USER->id
;
1107 if ($userrating != RATING_UNSET_RATING
) {
1108 $rating = new rating($ratingoptions);
1109 $rating->update_rating($userrating);
1110 } else { // Delete the rating if the user set to "Rate..."
1111 $options = new stdClass
;
1112 $options->contextid
= $context->id
;
1113 $options->component
= $component;
1114 $options->ratingarea
= $ratingarea;
1115 $options->userid
= $USER->id
;
1116 $options->itemid
= $itemid;
1118 $this->delete_ratings($options);
1121 // Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook.
1122 // Note that this would need to be done in both rate.php and rate_ajax.php.
1123 if ($context->contextlevel
== CONTEXT_MODULE
) {
1124 // Tell the module that its grades have changed.
1125 $modinstance = $DB->get_record($cm->modname
, array('id' => $cm->instance
));
1127 $modinstance->cmidnumber
= $cm->id
; // MDL-12961.
1128 $functionname = $cm->modname
.'_update_grades';
1129 require_once($CFG->dirroot
."/mod/{$cm->modname}/lib.php");
1130 if (function_exists($functionname)) {
1131 $functionname($modinstance, $rateduserid);
1136 // Object to return to client as JSON.
1137 $result->success
= true;
1139 // Need to retrieve the updated item to get its new aggregate value.
1140 $item = new stdClass
;
1141 $item->id
= $itemid;
1143 // Most of $ratingoptions variables were previously set.
1144 $ratingoptions->items
= array($item);
1145 $ratingoptions->aggregate
= $aggregationmethod;
1147 $items = $this->get_ratings($ratingoptions);
1148 $firstrating = $items[0]->rating
;
1150 // See if the user has permission to see the rating aggregate.
1151 if ($firstrating->user_can_view_aggregate()) {
1153 // For custom scales return text not the value.
1154 // This scales weirdness will go away when scales are refactored.
1156 $aggregatetoreturn = round($firstrating->aggregate
, 1);
1158 // Output a dash if aggregation method == COUNT as the count is output next to the aggregate anyway.
1159 if ($firstrating->settings
->aggregationmethod
== RATING_AGGREGATE_COUNT
or $firstrating->count
== 0) {
1160 $aggregatetoreturn = ' - ';
1161 } else if ($firstrating->settings
->scale
->id
< 0) { // If its non-numeric scale.
1162 // Dont use the scale item if the aggregation method is sum as adding items from a custom scale makes no sense.
1163 if ($firstrating->settings
->aggregationmethod
!= RATING_AGGREGATE_SUM
) {
1164 $scalerecord = $DB->get_record('scale', array('id' => -$firstrating->settings
->scale
->id
));
1166 $scalearray = explode(',', $scalerecord->scale
);
1167 $aggregatetoreturn = $scalearray[$aggregatetoreturn - 1];
1172 $result->aggregate
= $aggregatetoreturn;
1173 $result->count
= $firstrating->count
;
1174 $result->itemid
= $itemid;
1180 * Get ratings created since a given time.
1182 * @param stdClass $context context object
1183 * @param string $component component name
1184 * @param int $since the time to check
1185 * @return array list of ratings db records since the given timelimit
1188 public function get_component_ratings_since($context, $component, $since) {
1191 $ratingssince = array();
1192 $where = 'contextid = ? AND component = ? AND (timecreated > ? OR timemodified > ?)';
1193 $ratings = $DB->get_records_select('rating', $where, array($context->id
, $component, $since, $since));
1194 // Check area by area if we have permissions.
1195 $permissions = array();
1196 $rm = new rating_manager();
1198 foreach ($ratings as $rating) {
1199 // Check if the permission array for the area is cached.
1200 if (!isset($permissions[$rating->ratingarea
])) {
1201 $permissions[$rating->ratingarea
] = $rm->get_plugin_permissions_array($context->id
, $component,
1202 $rating->ratingarea
);
1205 if (($permissions[$rating->ratingarea
]['view'] and $rating->userid
== $USER->id
) or
1206 ($permissions[$rating->ratingarea
]['viewany'] or $permissions[$rating->ratingarea
]['viewall'])) {
1207 $ratingssince[$rating->id
] = $rating;
1210 return $ratingssince;
1212 } // End rating_manager class definition.
1215 * The rating_exception class for exceptions specific to the ratings system
1217 * @package core_rating
1219 * @copyright 2010 Andrew Davis
1220 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1223 class rating_exception
extends moodle_exception
{
1225 * @var string The message to accompany the thrown exception
1229 * Generate exceptions that can be easily identified as coming from the ratings system
1231 * @param string $errorcode the error code to generate
1233 public function __construct($errorcode) {
1234 $this->errorcode
= $errorcode;
1235 $this->message
= get_string($errorcode, 'error');