Merge branch 'w33_MDL-28709_m21_h304' of git://github.com/skodak/moodle into MOODLE_2...
[moodle.git] / rating / lib.php
blobf75a548438e011e9cf5765c7f4aad948de735b49
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * A class representing a single rating and containing some static methods for manipulating ratings
21 * @package core
22 * @subpackage rating
23 * @copyright 2010 Andrew Davis
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 define('RATING_UNSET_RATING', -999);
29 define ('RATING_AGGREGATE_NONE', 0); //no ratings
30 define ('RATING_AGGREGATE_AVERAGE', 1);
31 define ('RATING_AGGREGATE_COUNT', 2);
32 define ('RATING_AGGREGATE_MAXIMUM', 3);
33 define ('RATING_AGGREGATE_MINIMUM', 4);
34 define ('RATING_AGGREGATE_SUM', 5);
36 define ('RATING_DEFAULT_SCALE', 5);
38 /**
39 * The rating class represents a single rating by a single user
41 * @copyright 2010 Andrew Davis
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 * @since Moodle 2.0
45 class rating implements renderable {
47 /**
48 * The context in which this rating exists
49 * @var stdClass
51 public $context;
53 /**
54 * The component using ratings. For example "mod_forum"
55 * @var string
57 public $component;
59 /**
60 * The rating area to associate this rating with.
61 * This allows a plugin to rate more than one thing by specifying different rating areas.
62 * @var string
64 public $ratingarea = null;
66 /**
67 * The id of the item (forum post, glossary item etc) being rated
68 * @var int
70 public $itemid;
72 /**
73 * The id scale (1-5, 0-100) that was in use when the rating was submitted
74 * @var int
76 public $scaleid;
78 /**
79 * The id of the user who submitted the rating
80 * @var int
82 public $userid;
84 /**
85 * settings for this rating. Necessary to render the rating.
86 * @var stdclass
88 public $settings;
90 /**
91 * The Id of this rating within the rating table.
92 * This is only set if the rating already exists
93 * @var int
95 public $id = null;
97 /**
98 * The aggregate of the combined ratings for the associated item.
99 * This is only set if the rating already exists
101 * @var int
103 public $aggregate = null;
106 * The total number of ratings for the associated item.
107 * This is only set if the rating already exists
109 * @var int
111 public $count = 0;
114 * The rating the associated user gave the associated item
115 * This is only set if the rating already exists
117 * @var int
119 public $rating = null;
122 * The time the associated item was created
124 * @var int
126 public $itemtimecreated = null;
129 * The id of the user who submitted the rating
131 * @var int
133 public $itemuserid = null;
136 * Constructor.
137 * @param object $options {
138 * context => context context to use for the rating [required]
139 * component => component using ratings ie mod_forum [required]
140 * ratingarea => ratingarea to associate this rating with [required]
141 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
142 * scaleid => int The scale in use when the rating was submitted [required]
143 * userid => int The id of the user who submitted the rating [required]
144 * settings => Settings for the rating object [optional]
145 * id => The id of this rating (if the rating is from the db) [optional]
146 * aggregate => The aggregate for the rating [optional]
147 * count => The number of ratings [optional]
148 * rating => The rating given by the user [optional]
151 public function __construct($options) {
152 $this->context = $options->context;
153 $this->component = $options->component;
154 $this->ratingarea = $options->ratingarea;
155 $this->itemid = $options->itemid;
156 $this->scaleid = $options->scaleid;
157 $this->userid = $options->userid;
159 if (isset($options->settings)) {
160 $this->settings = $options->settings;
162 if (isset($options->id)) {
163 $this->id = $options->id;
165 if (isset($options->aggregate)) {
166 $this->aggregate = $options->aggregate;
168 if (isset($options->count)) {
169 $this->count = $options->count;
171 if (isset($options->rating)) {
172 $this->rating = $options->rating;
177 * Update this rating in the database
178 * @param int $rating the integer value of this rating
179 * @return void
181 public function update_rating($rating) {
182 global $DB;
184 $time = time();
186 $data = new stdClass;
187 $data->rating = $rating;
188 $data->timemodified = $time;
190 $item = new stdclass();
191 $item->id = $this->itemid;
192 $items = array($item);
194 $ratingoptions = new stdClass;
195 $ratingoptions->context = $this->context;
196 $ratingoptions->component = $this->component;
197 $ratingoptions->ratingarea = $this->ratingarea;
198 $ratingoptions->items = $items;
199 $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE;//we dont actually care what aggregation method is applied
200 $ratingoptions->scaleid = $this->scaleid;
201 $ratingoptions->userid = $this->userid;
203 $rm = new rating_manager();;
204 $items = $rm->get_ratings($ratingoptions);
205 $firstitem = $items[0]->rating;
207 if (empty($firstitem->id)) {
208 // Insert a new rating
209 $data->contextid = $this->context->id;
210 $data->component = $this->component;
211 $data->ratingarea = $this->ratingarea;
212 $data->rating = $rating;
213 $data->scaleid = $this->scaleid;
214 $data->userid = $this->userid;
215 $data->itemid = $this->itemid;
216 $data->timecreated = $time;
217 $data->timemodified = $time;
218 $DB->insert_record('rating', $data);
219 } else {
220 // Update the rating
221 $data->id = $firstitem->id;
222 $DB->update_record('rating', $data);
227 * Retreive the integer value of this rating
228 * @return int the integer value of this rating object
230 public function get_rating() {
231 return $this->rating;
235 * Returns this ratings aggregate value as a string.
237 * @return string
239 public function get_aggregate_string() {
241 $aggregate = $this->aggregate;
242 $method = $this->settings->aggregationmethod;
244 // only display aggregate if aggregation method isn't COUNT
245 $aggregatestr = '';
246 if ($aggregate && $method != RATING_AGGREGATE_COUNT) {
247 if ($method != RATING_AGGREGATE_SUM && !$this->settings->scale->isnumeric) {
248 $aggregatestr .= $this->settings->scale->scaleitems[round($aggregate)]; //round aggregate as we're using it as an index
249 } else { // aggregation is SUM or the scale is numeric
250 $aggregatestr .= round($aggregate, 1);
254 return $aggregatestr;
258 * Returns true if the user is able to rate this rating object
260 * @param int $userid Current user assumed if left empty
261 * @return bool
263 public function user_can_rate($userid = null) {
264 if (empty($userid)) {
265 global $USER;
266 $userid = $USER->id;
268 // You can't rate your item
269 if ($this->itemuserid == $userid) {
270 return false;
272 // You can't rate if you don't have the system cap
273 if (!$this->settings->permissions->rate) {
274 return false;
276 // You can't rate if you don't have the plugin cap
277 if (!$this->settings->pluginpermissions->rate) {
278 return false;
281 // You can't rate if the item was outside of the assessment times
282 $timestart = $this->settings->assesstimestart;
283 $timefinish = $this->settings->assesstimefinish;
284 $timecreated = $this->itemtimecreated;
285 if (!empty($timestart) && !empty($timefinish) && ($timecreated < $timestart || $timecreated > $timefinish)) {
286 return false;
288 return true;
292 * Returns true if the user is able to view the aggregate for this rating object.
294 * @param int|null $userid If left empty the current user is assumed.
295 * @return bool
297 public function user_can_view_aggregate($userid = null) {
298 if (empty($userid)) {
299 global $USER;
300 $userid = $USER->id;
303 // 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
304 // Note that viewany doesnt mean you can see the aggregate or ratings of your own items
305 if ((empty($this->itemuserid) or $this->itemuserid != $userid) && $this->settings->permissions->viewany && $this->settings->pluginpermissions->viewany ) {
306 return true;
309 // if its the current user's item and they have permission to view the aggregate on their own items
310 if ($this->itemuserid == $userid && $this->settings->permissions->view && $this->settings->pluginpermissions->view) {
311 return true;
314 return false;
318 * Returns a URL to view all of the ratings for the item this rating is for.
320 * If this is a rating of a post then this URL will take the user to a page that shows all
321 * of the ratings for the post (this one included).
323 * @param bool $popup
324 * @return moodle_url
326 public function get_view_ratings_url($popup = false) {
327 $attributes = array(
328 'contextid' => $this->context->id,
329 'component' => $this->component,
330 'ratingarea' => $this->ratingarea,
331 'itemid' => $this->itemid,
332 'scaleid' => $this->settings->scale->id
334 if ($popup) {
335 $attributes['popup'] = 1;
337 return new moodle_url('/rating/index.php', $attributes);
341 * Returns a URL that can be used to rate the associated item.
343 * @param int|null $rating The rating to give the item, if null then no rating
344 * param is added.
345 * @param moodle_url|string $returnurl The URL to return to.
346 * @return moodle_url
348 public function get_rate_url($rating = null, $returnurl = null) {
349 if (empty($returnurl)) {
350 if (!empty($this->settings->returnurl)) {
351 $returnurl = $this->settings->returnurl;
352 } else {
353 global $PAGE;
354 $returnurl = $PAGE->url;
357 $args = array(
358 'contextid' => $this->context->id,
359 'component' => $this->component,
360 'ratingarea' => $this->ratingarea,
361 'itemid' => $this->itemid,
362 'scaleid' => $this->settings->scale->id,
363 'returnurl' => $returnurl,
364 'rateduserid' => $this->itemuserid,
365 'aggregation' => $this->settings->aggregationmethod,
366 'sesskey' => sesskey()
368 if (!empty($rating)) {
369 $args['rating'] = $rating;
371 $url = new moodle_url('/rating/rate.php', $args);
372 return $url;
376 * Remove this rating from the database
377 * @return void
379 //public function delete_rating() {
380 //todo implement this if its actually needed
382 } //end rating class definition
385 * The rating_manager class provides the ability to retrieve sets of ratings from the database
387 * @copyright 2010 Andrew Davis
388 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
389 * @since Moodle 2.0
391 class rating_manager {
394 * An array of calculated scale options to save us generating them for each request.
395 * @var array
397 protected $scales = array();
400 * Gets set to true when the JavaScript that controls AJAX rating has been
401 * initialised (so that it only gets initialised once.
402 * @var int
404 protected $javascriptinitialised = false;
407 * Delete one or more ratings. Specify either a rating id, an item id or just the context id.
409 * @global moodle_database $DB
410 * @param stdClass $options {
411 * contextid => int the context in which the ratings exist [required]
412 * ratingid => int the id of an individual rating to delete [optional]
413 * userid => int delete the ratings submitted by this user. May be used in conjuction with itemid [optional]
414 * itemid => int delete all ratings attached to this item [optional]
415 * component => string The component to delete ratings from [optional]
416 * ratingarea => string The ratingarea to delete ratings from [optional]
418 * @return void
420 public function delete_ratings($options) {
421 global $DB;
423 if (empty($options->contextid)) {
424 throw new coding_exception('The context option is a required option when deleting ratings.');
427 $conditions = array('contextid' => $options->contextid);
428 $possibleconditions = array(
429 'ratingid' => 'id',
430 'userid' => 'userid',
431 'itemid' => 'itemid',
432 'component' => 'component',
433 'ratingarea' => 'ratingarea'
435 foreach ($possibleconditions as $option => $field) {
436 if (isset($options->{$option})) {
437 $conditions[$field] = $options->{$option};
440 $DB->delete_records('rating', $conditions);
444 * Returns an array of ratings for a given item (forum post, glossary entry etc)
445 * This returns all users ratings for a single item
446 * @param stdClass $options {
447 * context => context the context in which the ratings exists [required]
448 * component => component using ratings ie mod_forum [required]
449 * ratingarea => ratingarea to associate this rating with [required]
450 * itemid => int the id of the associated item (forum post, glossary item etc) [required]
451 * sort => string SQL sort by clause [optional]
453 * @return array an array of ratings
455 public function get_all_ratings_for_item($options) {
456 global $DB;
458 if (!isset($options->context)) {
459 throw new coding_exception('The context option is a required option when getting ratings for an item.');
461 if (!isset($options->itemid)) {
462 throw new coding_exception('The itemid option is a required option when getting ratings for an item.');
464 if (!isset($options->component)) {
465 throw new coding_exception('The component option is now a required option when getting ratings for an item.');
467 if (!isset($options->ratingarea)) {
468 throw new coding_exception('The ratingarea option is now a required option when getting ratings for an item.');
471 $sortclause = '';
472 if( !empty($options->sort) ) {
473 $sortclause = "ORDER BY $options->sort";
476 $params = array(
477 'contextid' => $options->context->id,
478 'itemid' => $options->itemid,
479 'component' => $options->component,
480 'ratingarea' => $options->ratingarea,
482 $userfields = user_picture::fields('u', null, 'userid');
483 $sql = "SELECT r.id, r.rating, r.itemid, r.userid, r.timemodified, r.component, r.ratingarea, $userfields
484 FROM {rating} r
485 LEFT JOIN {user} u ON r.userid = u.id
486 WHERE r.contextid = :contextid AND
487 r.itemid = :itemid AND
488 r.component = :component AND
489 r.ratingarea = :ratingarea
490 {$sortclause}";
492 return $DB->get_records_sql($sql, $params);
496 * Adds rating objects to an array of items (forum posts, glossary entries etc)
497 * Rating objects are available at $item->rating
498 * @param stdClass $options {
499 * context => context the context in which the ratings exists [required]
500 * component => the component name ie mod_forum [required]
501 * ratingarea => the ratingarea we are interested in [required]
502 * items => array an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id[required]
503 * aggregate => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
504 * scaleid => int the scale from which the user can select a rating [required]
505 * userid => int the id of the current user [optional]
506 * returnurl => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
507 * assesstimestart => int only allow rating of items created after this timestamp [optional]
508 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
509 * @return array the array of items with their ratings attached at $items[0]->rating
511 public function get_ratings($options) {
512 global $DB, $USER;
514 if (!isset($options->context)) {
515 throw new coding_exception('The context option is a required option when getting ratings.');
518 if (!isset($options->component)) {
519 throw new coding_exception('The component option is a required option when getting ratings.');
522 if (!isset($options->ratingarea)) {
523 throw new coding_exception('The ratingarea option is a required option when getting ratings.');
526 if (!isset($options->scaleid)) {
527 throw new coding_exception('The scaleid option is a required option when getting ratings.');
530 if (!isset($options->items)) {
531 throw new coding_exception('The items option is a required option when getting ratings.');
532 } else if (empty($options->items)) {
533 return array();
536 if (!isset($options->aggregate)) {
537 throw new coding_exception('The aggregate option is a required option when getting ratings.');
538 } else if ($options->aggregate == RATING_AGGREGATE_NONE) {
539 // Ratings arn't enabled.
540 return $options->items;
542 $aggregatestr = $this->get_aggregation_method($options->aggregate);
544 // Default the userid to the current user if it is not set
545 if (empty($options->userid)) {
546 $userid = $USER->id;
547 } else {
548 $userid = $options->userid;
551 // Get the item table name, the item id field, and the item user field for the given rating item
552 // from the related component.
553 list($type, $name) = normalize_component($options->component);
554 $default = array(null, 'id', 'userid');
555 list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type, $name, 'rating', 'get_item_fields', array($options), $default);
557 // Create an array of item ids
558 $itemids = array();
559 foreach ($options->items as $item) {
560 $itemids[] = $item->{$itemidcol};
563 // get the items from the database
564 list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
565 $params['contextid'] = $options->context->id;
566 $params['userid'] = $userid;
567 $params['component'] = $options->component;
568 $params['ratingarea'] = $options->ratingarea;
570 $sql = "SELECT r.id, r.itemid, r.userid, r.scaleid, r.rating AS usersrating
571 FROM {rating} r
572 WHERE r.userid = :userid AND
573 r.contextid = :contextid AND
574 r.itemid {$itemidtest} AND
575 r.component = :component AND
576 r.ratingarea = :ratingarea
577 ORDER BY r.itemid";
578 $userratings = $DB->get_records_sql($sql, $params);
580 $sql = "SELECT r.itemid, $aggregatestr(r.rating) AS aggrrating, COUNT(r.rating) AS numratings
581 FROM {rating} r
582 WHERE r.contextid = :contextid AND
583 r.itemid {$itemidtest} AND
584 r.component = :component AND
585 r.ratingarea = :ratingarea
586 GROUP BY r.itemid, r.component, r.ratingarea, r.contextid
587 ORDER BY r.itemid";
588 $aggregateratings = $DB->get_records_sql($sql, $params);
590 $ratingoptions = new stdClass;
591 $ratingoptions->context = $options->context;
592 $ratingoptions->component = $options->component;
593 $ratingoptions->ratingarea = $options->ratingarea;
594 $ratingoptions->settings = $this->generate_rating_settings_object($options);
595 foreach ($options->items as $item) {
596 $founduserrating = false;
597 foreach($userratings as $userrating) {
598 //look for an existing rating from this user of this item
599 if ($item->{$itemidcol} == $userrating->itemid) {
600 // Note: rec->scaleid = the id of scale at the time the rating was submitted
601 // may be different from the current scale id
602 $ratingoptions->scaleid = $userrating->scaleid;
603 $ratingoptions->userid = $userrating->userid;
604 $ratingoptions->id = $userrating->id;
605 $ratingoptions->rating = min($userrating->usersrating, $ratingoptions->settings->scale->max);
607 $founduserrating = true;
608 break;
611 if (!$founduserrating) {
612 $ratingoptions->scaleid = null;
613 $ratingoptions->userid = null;
614 $ratingoptions->id = null;
615 $ratingoptions->rating = null;
618 if (array_key_exists($item->{$itemidcol}, $aggregateratings)) {
619 $rec = $aggregateratings[$item->{$itemidcol}];
620 $ratingoptions->itemid = $item->{$itemidcol};
621 $ratingoptions->aggregate = min($rec->aggrrating, $ratingoptions->settings->scale->max);
622 $ratingoptions->count = $rec->numratings;
623 } else {
624 $ratingoptions->itemid = $item->{$itemidcol};
625 $ratingoptions->aggregate = null;
626 $ratingoptions->count = 0;
629 $rating = new rating($ratingoptions);
630 $rating->itemtimecreated = $this->get_item_time_created($item);
631 if (!empty($item->{$itemuseridcol})) {
632 $rating->itemuserid = $item->{$itemuseridcol};
634 $item->rating = $rating;
637 return $options->items;
641 * Generates a rating settings object based upon the options it is provided.
643 * @param stdClass $options {
644 * context => context the context in which the ratings exists [required]
645 * component => string The component the items belong to [required]
646 * ratingarea => string The ratingarea the items belong to [required]
647 * aggregate => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
648 * scaleid => int the scale from which the user can select a rating [required]
649 * returnurl => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
650 * assesstimestart => int only allow rating of items created after this timestamp [optional]
651 * assesstimefinish => int only allow rating of items created before this timestamp [optional]
652 * plugintype => string plugin type ie 'mod' Used to find the permissions callback [optional]
653 * pluginname => string plugin name ie 'forum' Used to find the permissions callback [optional]
655 * @return stdClass
657 protected function generate_rating_settings_object($options) {
659 if (!isset($options->context)) {
660 throw new coding_exception('The context option is a required option when generating a rating settings object.');
662 if (!isset($options->component)) {
663 throw new coding_exception('The component option is now a required option when generating a rating settings object.');
665 if (!isset($options->ratingarea)) {
666 throw new coding_exception('The ratingarea option is now a required option when generating a rating settings object.');
668 if (!isset($options->aggregate)) {
669 throw new coding_exception('The aggregate option is now a required option when generating a rating settings object.');
671 if (!isset($options->scaleid)) {
672 throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.');
675 // settings that are common to all ratings objects in this context
676 $settings = new stdClass;
677 $settings->scale = $this->generate_rating_scale_object($options->scaleid); // the scale to use now
678 $settings->aggregationmethod = $options->aggregate;
679 $settings->assesstimestart = null;
680 $settings->assesstimefinish = null;
682 // Collect options into the settings object
683 if (!empty($options->assesstimestart)) {
684 $settings->assesstimestart = $options->assesstimestart;
686 if (!empty($options->assesstimefinish)) {
687 $settings->assesstimefinish = $options->assesstimefinish;
689 if (!empty($options->returnurl)) {
690 $settings->returnurl = $options->returnurl;
693 // check site capabilities
694 $settings->permissions = new stdClass;
695 $settings->permissions->view = has_capability('moodle/rating:view', $options->context); // can view the aggregate of ratings of their own items
696 $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context); // can view the aggregate of ratings of other people's items
697 $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context); // can view individual ratings
698 $settings->permissions->rate = has_capability('moodle/rating:rate', $options->context); // can submit ratings
700 // check module capabilities (mostly for backwards compatability with old modules that previously implemented their own ratings)
701 $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, $options->component, $options->ratingarea);
702 $settings->pluginpermissions = new stdClass;
703 $settings->pluginpermissions->view = $pluginpermissionsarray['view'];
704 $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
705 $settings->pluginpermissions->viewall = $pluginpermissionsarray['viewall'];
706 $settings->pluginpermissions->rate = $pluginpermissionsarray['rate'];
708 return $settings;
712 * Generates a scale object that can be returned
714 * @global moodle_database $DB
715 * @param type $scaleid
716 * @return stdClass
718 protected function generate_rating_scale_object($scaleid) {
719 global $DB;
720 if (!array_key_exists('s'.$scaleid, $this->scales)) {
721 $scale = new stdClass;
722 $scale->id = $scaleid;
723 $scale->name = null;
724 $scale->courseid = null;
725 $scale->scaleitems = array();
726 $scale->isnumeric = true;
727 $scale->max = $scaleid;
729 if ($scaleid < 0) {
730 // It is a proper scale (not numeric)
731 $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid)));
732 if ($scalerecord) {
733 // We need to generate an array with string keys starting at 1
734 $scalearray = explode(',', $scalerecord->scale);
735 $c = count($scalearray);
736 for ($i = 0; $i < $c; $i++) {
737 // treat index as a string to allow sorting without changing the value
738 $scale->scaleitems[(string)($i + 1)] = $scalearray[$i];
740 krsort($scale->scaleitems); // have the highest grade scale item appear first
741 $scale->isnumeric = false;
742 $scale->name = $scalerecord->name;
743 $scale->courseid = $scalerecord->courseid;
744 $scale->max = count($scale->scaleitems);
746 } else {
747 //generate an array of values for numeric scales
748 for($i = 0; $i <= (int)$scaleid; $i++) {
749 $scale->scaleitems[(string)$i] = $i;
752 $this->scales['s'.$scaleid] = $scale;
754 return $this->scales['s'.$scaleid];
758 * Gets the time the given item was created
760 * TODO: Find a better solution for this, its not ideal to test for fields really we should be
761 * asking the component the item belongs to what field to look for or even the value we
762 * are looking for.
764 * @param stdClass $item
765 * @return mixed
767 protected function get_item_time_created($item) {
768 if( !empty($item->created) ) {
769 return $item->created;//the forum_posts table has created instead of timecreated
771 else if(!empty($item->timecreated)) {
772 return $item->timecreated;
774 else {
775 return null;
780 * Returns an array of grades calculated by aggregating item ratings.
781 * @param object $options {
782 * userid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
783 * aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
784 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
785 * itemtable => int the table containing the items [required]
786 * itemtableusercolum => int the column of the user table containing the item owner's user id [required]
787 * component => The component for the ratings [required]
788 * ratingarea => The ratingarea for the ratings [required]
790 * contextid => int the context in which the rated items exist [optional]
792 * modulename => string the name of the module [optional]
793 * moduleid => int the id of the module instance [optional]
795 * @return array the array of the user's grades
797 public function get_user_grades($options) {
798 global $DB;
800 $contextid = null;
802 if (!isset($options->component)) {
803 throw new coding_exception('The component option is now a required option when getting user grades from ratings.');
805 if (!isset($options->ratingarea)) {
806 throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.');
809 //if the calling code doesn't supply a context id we'll have to figure it out
810 if( !empty($options->contextid) ) {
811 $contextid = $options->contextid;
813 else if( !empty($options->cmid) ) {
814 //not implemented as not currently used although cmid is potentially available (the forum supplies it)
815 //Is there a convenient way to get a context id from a cm id?
816 //$cmidnumber = $options->cmidnumber;
818 else if ( !empty($options->modulename) && !empty($options->moduleid) ) {
819 $modulename = $options->modulename;
820 $moduleid = intval($options->moduleid);
822 //going direct to the db for the context id seems wrong
823 list($ctxselect, $ctxjoin) = context_instance_preload_sql('cm.id', CONTEXT_MODULE, 'ctx');
824 $sql = "SELECT cm.* $ctxselect
825 FROM {course_modules} cm
826 LEFT JOIN {modules} mo ON mo.id = cm.module
827 LEFT JOIN {{$modulename}} m ON m.id = cm.instance $ctxjoin
828 WHERE mo.name=:modulename AND
829 m.id=:moduleid";
830 $contextrecord = $DB->get_record_sql($sql, array('modulename'=>$modulename, 'moduleid'=>$moduleid), '*', MUST_EXIST);
831 $contextid = $contextrecord->ctxid;
834 $params = array();
835 $params['contextid'] = $contextid;
836 $params['component'] = $options->component;
837 $params['ratingarea'] = $options->ratingarea;
838 $itemtable = $options->itemtable;
839 $itemtableusercolumn = $options->itemtableusercolumn;
840 $scaleid = $options->scaleid;
841 $aggregationstring = $this->get_aggregation_method($options->aggregationmethod);
843 //if userid is not 0 we only want the grade for a single user
844 $singleuserwhere = '';
845 if ($options->userid != 0) {
846 $params['userid1'] = intval($options->userid);
847 $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
850 //MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)"
851 //r.contextid will be null for users who haven't been rated yet
852 //no longer including users who haven't been rated to reduce memory requirements
853 $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
854 FROM {user} u
855 LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
856 LEFT JOIN {rating} r ON r.itemid=i.id
857 WHERE r.contextid = :contextid AND
858 r.component = :component AND
859 r.ratingarea = :ratingarea
860 $singleuserwhere
861 GROUP BY u.id";
862 $results = $DB->get_records_sql($sql, $params);
864 if ($results) {
866 $scale = null;
867 $max = 0;
868 if ($options->scaleid >= 0) {
869 //numeric
870 $max = $options->scaleid;
871 } else {
872 //custom scales
873 $scale = $DB->get_record('scale', array('id' => -$options->scaleid));
874 if ($scale) {
875 $scale = explode(',', $scale->scale);
876 $max = count($scale);
877 } else {
878 debugging('rating_manager::get_user_grades() received a scale ID that doesnt exist');
882 // it could throw off the grading if count and sum returned a rawgrade higher than scale
883 // 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)
884 foreach ($results as $rid=>$result) {
885 if ($options->scaleid >= 0) {
886 //numeric
887 if ($result->rawgrade > $options->scaleid) {
888 $results[$rid]->rawgrade = $options->scaleid;
890 } else {
891 //scales
892 if (!empty($scale) && $result->rawgrade > $max) {
893 $results[$rid]->rawgrade = $max;
899 return $results;
903 * Returns array of aggregate types. Used by ratings.
905 * @return array
907 public function get_aggregate_types() {
908 return array (RATING_AGGREGATE_NONE => get_string('aggregatenone', 'rating'),
909 RATING_AGGREGATE_AVERAGE => get_string('aggregateavg', 'rating'),
910 RATING_AGGREGATE_COUNT => get_string('aggregatecount', 'rating'),
911 RATING_AGGREGATE_MAXIMUM => get_string('aggregatemax', 'rating'),
912 RATING_AGGREGATE_MINIMUM => get_string('aggregatemin', 'rating'),
913 RATING_AGGREGATE_SUM => get_string('aggregatesum', 'rating'));
917 * Converts an aggregation method constant into something that can be included in SQL
918 * @param int $aggregate An aggregation constant. For example, RATING_AGGREGATE_AVERAGE.
919 * @return string an SQL aggregation method
921 public function get_aggregation_method($aggregate) {
922 $aggregatestr = null;
923 switch($aggregate){
924 case RATING_AGGREGATE_AVERAGE:
925 $aggregatestr = 'AVG';
926 break;
927 case RATING_AGGREGATE_COUNT:
928 $aggregatestr = 'COUNT';
929 break;
930 case RATING_AGGREGATE_MAXIMUM:
931 $aggregatestr = 'MAX';
932 break;
933 case RATING_AGGREGATE_MINIMUM:
934 $aggregatestr = 'MIN';
935 break;
936 case RATING_AGGREGATE_SUM:
937 $aggregatestr = 'SUM';
938 break;
939 default:
940 $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270
941 debugging('Incorrect call to get_aggregation_method(), was called with incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
943 return $aggregatestr;
947 * Looks for a callback like forum_rating_permissions() to retrieve permissions from the plugin whose items are being rated
948 * @param int $contextid The current context id
949 * @param string component the name of the component that is using ratings ie 'mod_forum'
950 * @param string ratingarea The area the rating is associated with
951 * @return array rating related permissions
953 public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
954 $pluginpermissionsarray = null;
955 $defaultpluginpermissions = array('rate'=>false,'view'=>false,'viewany'=>false,'viewall'=>false);//deny by default
956 if (!empty($component)) {
957 list($type, $name) = normalize_component($component);
958 $pluginpermissionsarray = plugin_callback($type, $name, 'rating', 'permissions', array($contextid, $component, $ratingarea), $defaultpluginpermissions);
959 } else {
960 $pluginpermissionsarray = $defaultpluginpermissions;
962 return $pluginpermissionsarray;
966 * Validates a submitted rating
967 * @param array $params submitted data
968 * context => object the context in which the rated items exists [required]
969 * component => The component the rating belongs to [required]
970 * ratingarea => The ratingarea the rating is associated with [required]
971 * itemid => int the ID of the object being rated [required]
972 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
973 * rating => int the submitted rating
974 * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
975 * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
976 * @return boolean true if the rating is valid. False if callback wasnt found and will throw rating_exception if rating is invalid
978 public function check_rating_is_valid($params) {
980 if (!isset($params['context'])) {
981 throw new coding_exception('The context option is a required option when checking rating validity.');
983 if (!isset($params['component'])) {
984 throw new coding_exception('The component option is now a required option when checking rating validity');
986 if (!isset($params['ratingarea'])) {
987 throw new coding_exception('The ratingarea option is now a required option when checking rating validity');
989 if (!isset($params['itemid'])) {
990 throw new coding_exception('The itemid option is now a required option when checking rating validity');
992 if (!isset($params['scaleid'])) {
993 throw new coding_exception('The scaleid option is now a required option when checking rating validity');
995 if (!isset($params['rateduserid'])) {
996 throw new coding_exception('The rateduserid option is now a required option when checking rating validity');
999 list($plugintype, $pluginname) = normalize_component($params['component']);
1001 //this looks for a function like forum_rating_validate() in mod_forum lib.php
1002 //wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments
1003 $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
1005 //if null then the callback doesn't exist
1006 if ($isvalid === null) {
1007 $isvalid = false;
1008 debugging('rating validation callback not found for component '. clean_param($component, PARAM_ALPHANUMEXT));
1010 return $isvalid;
1014 * Initialises JavaScript to enable AJAX ratings on the provided page
1016 * @param moodle_page $page
1017 * @return true
1019 public function initialise_rating_javascript(moodle_page $page) {
1020 global $CFG;
1022 if ($this->javascriptinitialised) {
1023 return true;
1026 if (!empty($CFG->enableajax)) {
1027 $page->requires->js_init_call('M.core_rating.init');
1030 $this->javascriptinitialised = true;
1031 return true;
1035 * Returns a string that describes the aggregation method that was provided.
1037 * @param string $aggregationmethod
1038 * @return string
1040 public function get_aggregate_label($aggregationmethod) {
1041 $aggregatelabel = '';
1042 switch ($aggregationmethod) {
1043 case RATING_AGGREGATE_AVERAGE :
1044 $aggregatelabel .= get_string("aggregateavg", "rating");
1045 break;
1046 case RATING_AGGREGATE_COUNT :
1047 $aggregatelabel .= get_string("aggregatecount", "rating");
1048 break;
1049 case RATING_AGGREGATE_MAXIMUM :
1050 $aggregatelabel .= get_string("aggregatemax", "rating");
1051 break;
1052 case RATING_AGGREGATE_MINIMUM :
1053 $aggregatelabel .= get_string("aggregatemin", "rating");
1054 break;
1055 case RATING_AGGREGATE_SUM :
1056 $aggregatelabel .= get_string("aggregatesum", "rating");
1057 break;
1059 $aggregatelabel .= get_string('labelsep', 'langconfig');
1060 return $aggregatelabel;
1063 }//end rating_manager class definition
1065 class rating_exception extends moodle_exception {
1066 public $message;
1067 function __construct($errorcode) {
1068 $this->errorcode = $errorcode;
1069 $this->message = get_string($errorcode, 'error');